Merge network namespaces work into master (#6046)
Merge network namespaces work into master
This commit is contained in:
commit
34deb47a8b
|
@ -459,6 +459,7 @@ type AllocatedTaskResources struct {
|
|||
|
||||
type AllocatedSharedResources struct {
|
||||
DiskMB int64
|
||||
Networks []*NetworkResource
|
||||
}
|
||||
|
||||
type AllocatedCpuResources struct {
|
||||
|
|
|
@ -20,7 +20,7 @@ func TestCompose(t *testing.T) {
|
|||
{
|
||||
CIDR: "0.0.0.0/0",
|
||||
MBits: intToPtr(100),
|
||||
ReservedPorts: []Port{{"", 80}, {"", 443}},
|
||||
ReservedPorts: []Port{{"", 80, 0}, {"", 443, 0}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
@ -111,8 +111,8 @@ func TestCompose(t *testing.T) {
|
|||
CIDR: "0.0.0.0/0",
|
||||
MBits: intToPtr(100),
|
||||
ReservedPorts: []Port{
|
||||
{"", 80},
|
||||
{"", 443},
|
||||
{"", 80, 0},
|
||||
{"", 443, 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -86,17 +86,20 @@ func (r *Resources) Merge(other *Resources) {
|
|||
type Port struct {
|
||||
Label string
|
||||
Value int `mapstructure:"static"`
|
||||
To int `mapstructure:"to"`
|
||||
}
|
||||
|
||||
// NetworkResource is used to describe required network
|
||||
// resources of a given task.
|
||||
type NetworkResource struct {
|
||||
Mode string
|
||||
Device string
|
||||
CIDR string
|
||||
IP string
|
||||
MBits *int
|
||||
ReservedPorts []Port
|
||||
DynamicPorts []Port
|
||||
Services []*Service
|
||||
}
|
||||
|
||||
func (n *NetworkResource) Canonicalize() {
|
||||
|
|
25
api/tasks.go
25
api/tasks.go
|
@ -372,6 +372,7 @@ type Service struct {
|
|||
AddressMode string `mapstructure:"address_mode"`
|
||||
Checks []ServiceCheck
|
||||
CheckRestart *CheckRestart `mapstructure:"check_restart"`
|
||||
Connect *ConsulConnect
|
||||
}
|
||||
|
||||
func (s *Service) Canonicalize(t *Task, tg *TaskGroup, job *Job) {
|
||||
|
@ -392,6 +393,25 @@ func (s *Service) Canonicalize(t *Task, tg *TaskGroup, job *Job) {
|
|||
}
|
||||
}
|
||||
|
||||
type ConsulConnect struct {
|
||||
SidecarService *ConsulSidecarService `mapstructure:"sidecar_service"`
|
||||
}
|
||||
|
||||
type ConsulSidecarService struct {
|
||||
Port string
|
||||
Proxy *ConsulProxy
|
||||
}
|
||||
|
||||
type ConsulProxy struct {
|
||||
Upstreams []*ConsulUpstream
|
||||
}
|
||||
|
||||
type ConsulUpstream struct {
|
||||
//FIXME Pointers?
|
||||
DestinationName string `mapstructure:"destination_name"`
|
||||
LocalBindPort int `mapstructure:"local_bind_port"`
|
||||
}
|
||||
|
||||
// EphemeralDisk is an ephemeral disk object
|
||||
type EphemeralDisk struct {
|
||||
Sticky *bool
|
||||
|
@ -493,7 +513,9 @@ type TaskGroup struct {
|
|||
EphemeralDisk *EphemeralDisk
|
||||
Update *UpdateStrategy
|
||||
Migrate *MigrateStrategy
|
||||
Networks []*NetworkResource
|
||||
Meta map[string]string
|
||||
Services []*Service
|
||||
}
|
||||
|
||||
// NewTaskGroup creates a new TaskGroup.
|
||||
|
@ -604,6 +626,9 @@ func (g *TaskGroup) Canonicalize(job *Job) {
|
|||
for _, a := range g.Affinities {
|
||||
a.Canonicalize()
|
||||
}
|
||||
for _, n := range g.Networks {
|
||||
n.Canonicalize()
|
||||
}
|
||||
}
|
||||
|
||||
// Constrain is used to add a constraint to a task group.
|
||||
|
|
|
@ -269,7 +269,7 @@ func TestTask_Require(t *testing.T) {
|
|||
{
|
||||
CIDR: "0.0.0.0/0",
|
||||
MBits: intToPtr(100),
|
||||
ReservedPorts: []Port{{"", 80}, {"", 443}},
|
||||
ReservedPorts: []Port{{"", 80, 0}, {"", 443, 0}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -238,7 +238,12 @@ func (t *Tracker) watchTaskEvents() {
|
|||
// Store the task states
|
||||
t.l.Lock()
|
||||
for task, state := range alloc.TaskStates {
|
||||
t.taskHealth[task].state = state
|
||||
//TODO(schmichael) for now skip unknown tasks as
|
||||
//they're task group services which don't currently
|
||||
//support checks anyway
|
||||
if v, ok := t.taskHealth[task]; ok {
|
||||
v.state = state
|
||||
}
|
||||
}
|
||||
t.l.Unlock()
|
||||
|
||||
|
@ -355,7 +360,12 @@ OUTER:
|
|||
// Store the task registrations
|
||||
t.l.Lock()
|
||||
for task, reg := range allocReg.Tasks {
|
||||
t.taskHealth[task].taskRegistrations = reg
|
||||
//TODO(schmichael) for now skip unknown tasks as
|
||||
//they're task group services which don't currently
|
||||
//support checks anyway
|
||||
if v, ok := t.taskHealth[task]; ok {
|
||||
v.taskRegistrations = reg
|
||||
}
|
||||
}
|
||||
t.l.Unlock()
|
||||
|
||||
|
|
|
@ -185,7 +185,9 @@ func NewAllocRunner(config *Config) (*allocRunner, error) {
|
|||
ar.allocDir = allocdir.NewAllocDir(ar.logger, filepath.Join(config.ClientConfig.AllocDir, alloc.ID))
|
||||
|
||||
// Initialize the runners hooks.
|
||||
ar.initRunnerHooks()
|
||||
if err := ar.initRunnerHooks(config.ClientConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create the TaskRunners
|
||||
if err := ar.initTaskRunners(tg.Tasks); err != nil {
|
||||
|
|
|
@ -6,9 +6,28 @@ import (
|
|||
|
||||
multierror "github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/nomad/client/allocrunner/interfaces"
|
||||
clientconfig "github.com/hashicorp/nomad/client/config"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/plugins/drivers"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// allocHealthSetter is a shim to allow the alloc health watcher hook to set
|
||||
// and clear the alloc health without full access to the alloc runner state
|
||||
type allocHealthSetter struct {
|
||||
|
@ -76,12 +95,24 @@ func (a *allocHealthSetter) SetHealth(healthy, isDeploy bool, trackerTaskEvents
|
|||
}
|
||||
|
||||
// initRunnerHooks intializes the runners hooks.
|
||||
func (ar *allocRunner) initRunnerHooks() {
|
||||
func (ar *allocRunner) initRunnerHooks(config *clientconfig.Config) error {
|
||||
hookLogger := ar.logger.Named("runner_hook")
|
||||
|
||||
// create health setting shim
|
||||
hs := &allocHealthSetter{ar}
|
||||
|
||||
// create network isolation setting shim
|
||||
ns := &allocNetworkIsolationSetter{ar: ar}
|
||||
|
||||
// build the network manager
|
||||
nm, err := newNetworkManager(ar.Alloc(), ar.driverManager)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to configure network manager: %v", err)
|
||||
}
|
||||
|
||||
// create network configurator
|
||||
nc := newNetworkConfigurator(ar.Alloc(), config)
|
||||
|
||||
// Create the alloc directory hook. This is run first to ensure the
|
||||
// directory path exists for other hooks.
|
||||
ar.runnerHooks = []interfaces.RunnerHook{
|
||||
|
@ -89,7 +120,10 @@ func (ar *allocRunner) initRunnerHooks() {
|
|||
newUpstreamAllocsHook(hookLogger, ar.prevAllocWatcher),
|
||||
newDiskMigrationHook(hookLogger, ar.prevAllocMigrator, ar.allocDir),
|
||||
newAllocHealthWatcherHook(hookLogger, ar.Alloc(), hs, ar.Listener(), ar.consulClient),
|
||||
newNetworkHook(hookLogger, ns, ar.Alloc(), nm, nc),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// prerun is used to run the runners prerun hooks.
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
package allocrunner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
hclog "github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/plugins/drivers"
|
||||
)
|
||||
|
||||
// networkHook is an alloc lifecycle hook that manages the network namespace
|
||||
// for an alloc
|
||||
type networkHook struct {
|
||||
// setter is a callback to set the network isolation spec when after the
|
||||
// network is created
|
||||
setter networkIsolationSetter
|
||||
|
||||
// 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
|
||||
|
||||
logger hclog.Logger
|
||||
}
|
||||
|
||||
func newNetworkHook(logger hclog.Logger, ns networkIsolationSetter,
|
||||
alloc *structs.Allocation, netManager drivers.DriverNetworkManager,
|
||||
netConfigurator NetworkConfigurator) *networkHook {
|
||||
return &networkHook{
|
||||
setter: ns,
|
||||
alloc: alloc,
|
||||
manager: netManager,
|
||||
networkConfigurator: netConfigurator,
|
||||
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
|
||||
}
|
||||
|
||||
spec, err := h.manager.CreateNetwork(h.alloc.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create network for alloc: %v", err)
|
||||
}
|
||||
|
||||
if spec != nil {
|
||||
h.spec = spec
|
||||
h.setter.SetNetworkIsolation(spec)
|
||||
}
|
||||
|
||||
if err := h.networkConfigurator.Setup(h.alloc, spec); err != nil {
|
||||
return fmt.Errorf("failed to configure networking for alloc: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *networkHook) Postrun() error {
|
||||
if h.spec == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := h.networkConfigurator.Teardown(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)
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package allocrunner
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/client/allocrunner/interfaces"
|
||||
"github.com/hashicorp/nomad/helper/testlog"
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/plugins/drivers"
|
||||
"github.com/hashicorp/nomad/plugins/drivers/testutils"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// statically assert network hook implements the expected interfaces
|
||||
var _ interfaces.RunnerPrerunHook = (*networkHook)(nil)
|
||||
var _ interfaces.RunnerPostrunHook = (*networkHook)(nil)
|
||||
|
||||
type mockNetworkIsolationSetter struct {
|
||||
t *testing.T
|
||||
expectedSpec *drivers.NetworkIsolationSpec
|
||||
called bool
|
||||
}
|
||||
|
||||
func (m *mockNetworkIsolationSetter) SetNetworkIsolation(spec *drivers.NetworkIsolationSpec) {
|
||||
m.called = true
|
||||
require.Exactly(m.t, m.expectedSpec, spec)
|
||||
}
|
||||
|
||||
// Test that the prerun and postrun hooks call the setter with the expected spec when
|
||||
// the network mode is not host
|
||||
func TestNetworkHook_Prerun_Postrun(t *testing.T) {
|
||||
alloc := mock.Alloc()
|
||||
alloc.Job.TaskGroups[0].Networks = []*structs.NetworkResource{
|
||||
{
|
||||
Mode: "bridge",
|
||||
},
|
||||
}
|
||||
spec := &drivers.NetworkIsolationSpec{
|
||||
Mode: drivers.NetIsolationModeGroup,
|
||||
Path: "test",
|
||||
Labels: map[string]string{"abc": "123"},
|
||||
}
|
||||
|
||||
destroyCalled := false
|
||||
nm := &testutils.MockDriver{
|
||||
MockNetworkManager: testutils.MockNetworkManager{
|
||||
CreateNetworkF: func(allocID string) (*drivers.NetworkIsolationSpec, error) {
|
||||
require.Equal(t, alloc.ID, allocID)
|
||||
return spec, nil
|
||||
},
|
||||
|
||||
DestroyNetworkF: func(allocID string, netSpec *drivers.NetworkIsolationSpec) error {
|
||||
destroyCalled = true
|
||||
require.Equal(t, alloc.ID, allocID)
|
||||
require.Exactly(t, spec, netSpec)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
setter := &mockNetworkIsolationSetter{
|
||||
t: t,
|
||||
expectedSpec: spec,
|
||||
}
|
||||
require := require.New(t)
|
||||
|
||||
logger := testlog.HCLogger(t)
|
||||
hook := newNetworkHook(logger, setter, alloc, nm, &hostNetworkConfigurator{})
|
||||
require.NoError(hook.Prerun())
|
||||
require.True(setter.called)
|
||||
require.False(destroyCalled)
|
||||
require.NoError(hook.Postrun())
|
||||
require.True(destroyCalled)
|
||||
|
||||
// reset and use host network mode
|
||||
setter.called = false
|
||||
destroyCalled = false
|
||||
alloc.Job.TaskGroups[0].Networks[0].Mode = "host"
|
||||
hook = newNetworkHook(logger, setter, alloc, nm, &hostNetworkConfigurator{})
|
||||
require.NoError(hook.Prerun())
|
||||
require.False(setter.called)
|
||||
require.False(destroyCalled)
|
||||
require.NoError(hook.Postrun())
|
||||
require.False(destroyCalled)
|
||||
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
package allocrunner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
clientconfig "github.com/hashicorp/nomad/client/config"
|
||||
"github.com/hashicorp/nomad/client/lib/nsutil"
|
||||
"github.com/hashicorp/nomad/client/pluginmanager/drivermanager"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/plugins/drivers"
|
||||
)
|
||||
|
||||
func newNetworkManager(alloc *structs.Allocation, driverManager drivermanager.Manager) (nm drivers.DriverNetworkManager, err error) {
|
||||
// The defaultNetworkManager is used if a driver doesn't need to create the network
|
||||
nm = &defaultNetworkManager{}
|
||||
tg := alloc.Job.LookupTaskGroup(alloc.TaskGroup)
|
||||
|
||||
// default netmode to host, this can be overridden by the task or task group
|
||||
tgNetMode := "host"
|
||||
if len(tg.Networks) > 0 && tg.Networks[0].Mode != "" {
|
||||
tgNetMode = tg.Networks[0].Mode
|
||||
}
|
||||
|
||||
// networkInitiator tracks the task driver which needs to create the network
|
||||
// to check for multiple drivers needing the create the network
|
||||
var networkInitiator string
|
||||
|
||||
// driverCaps tracks which drivers we've checked capabilities for so as not
|
||||
// to do extra work
|
||||
driverCaps := make(map[string]struct{})
|
||||
for _, task := range tg.Tasks {
|
||||
// the task's netmode defaults to the the task group but can be overridden
|
||||
taskNetMode := tgNetMode
|
||||
if len(task.Resources.Networks) > 0 && task.Resources.Networks[0].Mode != "" {
|
||||
taskNetMode = task.Resources.Networks[0].Mode
|
||||
}
|
||||
|
||||
// netmode host should always work to support backwards compat
|
||||
if taskNetMode == "host" {
|
||||
continue
|
||||
}
|
||||
|
||||
// check to see if capabilities of this task's driver have already been checked
|
||||
if _, ok := driverCaps[task.Driver]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
driver, err := driverManager.Dispense(task.Driver)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to dispense driver %s: %v", task.Driver, err)
|
||||
}
|
||||
|
||||
caps, err := driver.Capabilities()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrive capabilities for driver %s: %v",
|
||||
task.Driver, err)
|
||||
}
|
||||
|
||||
// check that the driver supports the requested network isolation mode
|
||||
netIsolationMode := netModeToIsolationMode(taskNetMode)
|
||||
if !caps.HasNetIsolationMode(netIsolationMode) {
|
||||
return nil, fmt.Errorf("task %s does not support %q networking mode", task.Name, taskNetMode)
|
||||
}
|
||||
|
||||
// check if the driver needs to create the network and if a different
|
||||
// driver has already claimed it needs to initiate the network
|
||||
if caps.MustInitiateNetwork {
|
||||
if networkInitiator != "" {
|
||||
return nil, fmt.Errorf("tasks %s and %s want to initiate networking but only one driver can do so", networkInitiator, task.Name)
|
||||
}
|
||||
netManager, ok := driver.(drivers.DriverNetworkManager)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("driver %s does not implement network management RPCs", task.Driver)
|
||||
}
|
||||
|
||||
nm = netManager
|
||||
networkInitiator = task.Name
|
||||
}
|
||||
|
||||
// mark this driver's capabilities as checked
|
||||
driverCaps[task.Driver] = struct{}{}
|
||||
}
|
||||
|
||||
return nm, nil
|
||||
}
|
||||
|
||||
// defaultNetworkManager creates a network namespace for the alloc
|
||||
type defaultNetworkManager struct{}
|
||||
|
||||
func (*defaultNetworkManager) CreateNetwork(allocID string) (*drivers.NetworkIsolationSpec, error) {
|
||||
netns, err := nsutil.NewNS(allocID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
spec := &drivers.NetworkIsolationSpec{
|
||||
Mode: drivers.NetIsolationModeGroup,
|
||||
Path: netns.Path(),
|
||||
Labels: make(map[string]string),
|
||||
}
|
||||
|
||||
return spec, nil
|
||||
}
|
||||
|
||||
func (*defaultNetworkManager) DestroyNetwork(allocID string, spec *drivers.NetworkIsolationSpec) error {
|
||||
return nsutil.UnmountNS(spec.Path)
|
||||
}
|
||||
|
||||
func netModeToIsolationMode(netMode string) drivers.NetIsolationMode {
|
||||
switch strings.ToLower(netMode) {
|
||||
case "host":
|
||||
return drivers.NetIsolationModeHost
|
||||
case "bridge", "none":
|
||||
return drivers.NetIsolationModeGroup
|
||||
case "driver":
|
||||
return drivers.NetIsolationModeTask
|
||||
default:
|
||||
return drivers.NetIsolationModeHost
|
||||
}
|
||||
}
|
||||
|
||||
func newNetworkConfigurator(alloc *structs.Allocation, config *clientconfig.Config) NetworkConfigurator {
|
||||
tg := alloc.Job.LookupTaskGroup(alloc.TaskGroup)
|
||||
|
||||
// Check if network stanza is given
|
||||
if len(tg.Networks) == 0 {
|
||||
return &hostNetworkConfigurator{}
|
||||
}
|
||||
|
||||
switch strings.ToLower(tg.Networks[0].Mode) {
|
||||
case "bridge":
|
||||
return newBridgeNetworkConfigurator(context.Background(), config.BridgeNetworkName, config.BridgeNetworkAllocSubnet, config.CNIPath)
|
||||
default:
|
||||
return &hostNetworkConfigurator{}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
package allocrunner
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/client/pluginmanager"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/plugins/drivers"
|
||||
"github.com/hashicorp/nomad/plugins/drivers/testutils"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var mockDrivers = map[string]drivers.DriverPlugin{
|
||||
"hostonly": &testutils.MockDriver{
|
||||
CapabilitiesF: func() (*drivers.Capabilities, error) {
|
||||
return &drivers.Capabilities{
|
||||
NetIsolationModes: []drivers.NetIsolationMode{drivers.NetIsolationModeHost},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
"group1": &testutils.MockDriver{
|
||||
CapabilitiesF: func() (*drivers.Capabilities, error) {
|
||||
return &drivers.Capabilities{
|
||||
NetIsolationModes: []drivers.NetIsolationMode{
|
||||
drivers.NetIsolationModeHost, drivers.NetIsolationModeGroup},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
"group2": &testutils.MockDriver{
|
||||
CapabilitiesF: func() (*drivers.Capabilities, error) {
|
||||
return &drivers.Capabilities{
|
||||
NetIsolationModes: []drivers.NetIsolationMode{
|
||||
drivers.NetIsolationModeHost, drivers.NetIsolationModeGroup},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
"mustinit1": &testutils.MockDriver{
|
||||
CapabilitiesF: func() (*drivers.Capabilities, error) {
|
||||
return &drivers.Capabilities{
|
||||
NetIsolationModes: []drivers.NetIsolationMode{
|
||||
drivers.NetIsolationModeHost, drivers.NetIsolationModeGroup},
|
||||
MustInitiateNetwork: true,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
"mustinit2": &testutils.MockDriver{
|
||||
CapabilitiesF: func() (*drivers.Capabilities, error) {
|
||||
return &drivers.Capabilities{
|
||||
NetIsolationModes: []drivers.NetIsolationMode{
|
||||
drivers.NetIsolationModeHost, drivers.NetIsolationModeGroup},
|
||||
MustInitiateNetwork: true,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type mockDriverManager struct {
|
||||
pluginmanager.MockPluginManager
|
||||
}
|
||||
|
||||
func (m *mockDriverManager) Dispense(driver string) (drivers.DriverPlugin, error) {
|
||||
return mockDrivers[driver], nil
|
||||
}
|
||||
|
||||
func TestNewNetworkManager(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
alloc *structs.Allocation
|
||||
err bool
|
||||
mustInit bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "defaults/backwards compat",
|
||||
alloc: &structs.Allocation{
|
||||
TaskGroup: "group",
|
||||
Job: &structs.Job{
|
||||
TaskGroups: []*structs.TaskGroup{
|
||||
{
|
||||
Name: "group",
|
||||
Networks: []*structs.NetworkResource{},
|
||||
Tasks: []*structs.Task{
|
||||
{
|
||||
Name: "task1",
|
||||
Driver: "group1",
|
||||
Resources: &structs.Resources{},
|
||||
},
|
||||
{
|
||||
Name: "task2",
|
||||
Driver: "group2",
|
||||
Resources: &structs.Resources{},
|
||||
},
|
||||
{
|
||||
Name: "task3",
|
||||
Driver: "mustinit1",
|
||||
Resources: &structs.Resources{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "driver /w must init network",
|
||||
alloc: &structs.Allocation{
|
||||
TaskGroup: "group",
|
||||
Job: &structs.Job{
|
||||
TaskGroups: []*structs.TaskGroup{
|
||||
{
|
||||
Name: "group",
|
||||
Networks: []*structs.NetworkResource{
|
||||
{
|
||||
Mode: "bridge",
|
||||
},
|
||||
},
|
||||
Tasks: []*structs.Task{
|
||||
{
|
||||
Name: "task1",
|
||||
Driver: "group1",
|
||||
Resources: &structs.Resources{},
|
||||
},
|
||||
{
|
||||
Name: "task2",
|
||||
Driver: "mustinit2",
|
||||
Resources: &structs.Resources{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mustInit: true,
|
||||
},
|
||||
{
|
||||
name: "multiple mustinit",
|
||||
alloc: &structs.Allocation{
|
||||
TaskGroup: "group",
|
||||
Job: &structs.Job{
|
||||
TaskGroups: []*structs.TaskGroup{
|
||||
{
|
||||
Name: "group",
|
||||
Networks: []*structs.NetworkResource{
|
||||
{
|
||||
Mode: "bridge",
|
||||
},
|
||||
},
|
||||
Tasks: []*structs.Task{
|
||||
{
|
||||
Name: "task1",
|
||||
Driver: "mustinit1",
|
||||
Resources: &structs.Resources{},
|
||||
},
|
||||
{
|
||||
Name: "task2",
|
||||
Driver: "mustinit2",
|
||||
Resources: &structs.Resources{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
err: true,
|
||||
errContains: "want to initiate networking but only one",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
nm, err := newNetworkManager(tc.alloc, &mockDriverManager{})
|
||||
if tc.err {
|
||||
require.Error(err)
|
||||
require.Contains(err.Error(), tc.errContains)
|
||||
} else {
|
||||
require.NoError(err)
|
||||
}
|
||||
|
||||
if tc.mustInit {
|
||||
_, ok := nm.(*testutils.MockDriver)
|
||||
require.True(ok)
|
||||
} else if tc.err {
|
||||
require.Nil(nm)
|
||||
} else {
|
||||
_, ok := nm.(*defaultNetworkManager)
|
||||
require.True(ok)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
//+build !linux
|
||||
|
||||
package allocrunner
|
||||
|
||||
import (
|
||||
clientconfig "github.com/hashicorp/nomad/client/config"
|
||||
"github.com/hashicorp/nomad/client/pluginmanager/drivermanager"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/plugins/drivers"
|
||||
)
|
||||
|
||||
// TODO: Support windows shared networking
|
||||
func newNetworkManager(alloc *structs.Allocation, driverManager drivermanager.Manager) (nm drivers.DriverNetworkManager, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func newNetworkConfigurator(alloc *structs.Allocation, config *clientconfig.Config) NetworkConfigurator {
|
||||
return &hostNetworkConfigurator{}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package allocrunner
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/plugins/drivers"
|
||||
)
|
||||
|
||||
// NetworkConfigurator sets up and tears down the interfaces, routes, firewall
|
||||
// rules, etc for the configured networking mode of the allocation.
|
||||
type NetworkConfigurator interface {
|
||||
Setup(*structs.Allocation, *drivers.NetworkIsolationSpec) error
|
||||
Teardown(*structs.Allocation, *drivers.NetworkIsolationSpec) error
|
||||
}
|
||||
|
||||
// hostNetworkConfigurator is a noop implementation of a NetworkConfigurator for
|
||||
// when the alloc join's a client host's network namespace and thus does not
|
||||
// require further configuration
|
||||
type hostNetworkConfigurator struct{}
|
||||
|
||||
func (h *hostNetworkConfigurator) Setup(*structs.Allocation, *drivers.NetworkIsolationSpec) error {
|
||||
return nil
|
||||
}
|
||||
func (h *hostNetworkConfigurator) Teardown(*structs.Allocation, *drivers.NetworkIsolationSpec) error {
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
package allocrunner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/containernetworking/cni/libcni"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/plugins/drivers"
|
||||
)
|
||||
|
||||
const (
|
||||
// envCNIPath is the environment variable name to use to derive the CNI path
|
||||
// when it is not explicitly set by the client
|
||||
envCNIPath = "CNI_PATH"
|
||||
|
||||
// defaultCNIPath is the CNI path to use when it is not set by the client
|
||||
// and is not set by environment variable
|
||||
defaultCNIPath = "/opt/cni/bin"
|
||||
|
||||
// defaultNomadBridgeName is the name of the bridge to use when not set by
|
||||
// the client
|
||||
defaultNomadBridgeName = "nomad"
|
||||
|
||||
// bridgeNetworkAllocIfName is the name that is set for the interface created
|
||||
// inside of the alloc network which is connected to the bridge
|
||||
bridgeNetworkContainerIfName = "eth0"
|
||||
|
||||
// defaultNomadAllocSubnet is the subnet to use for host local ip address
|
||||
// allocation when not specified by the client
|
||||
defaultNomadAllocSubnet = "172.26.66.0/23"
|
||||
)
|
||||
|
||||
// bridgeNetworkConfigurator is a NetworkConfigurator which adds the alloc to a
|
||||
// shared bridge, configures masquerading for egress traffic and port mapping
|
||||
// for ingress
|
||||
type bridgeNetworkConfigurator struct {
|
||||
ctx context.Context
|
||||
cniConfig *libcni.CNIConfig
|
||||
allocSubnet string
|
||||
bridgeName string
|
||||
}
|
||||
|
||||
func newBridgeNetworkConfigurator(ctx context.Context, bridgeName, ipRange, cniPath string) *bridgeNetworkConfigurator {
|
||||
b := &bridgeNetworkConfigurator{
|
||||
ctx: ctx,
|
||||
bridgeName: bridgeName,
|
||||
allocSubnet: ipRange,
|
||||
}
|
||||
if cniPath == "" {
|
||||
if cniPath = os.Getenv(envCNIPath); cniPath == "" {
|
||||
cniPath = defaultCNIPath
|
||||
}
|
||||
}
|
||||
b.cniConfig = libcni.NewCNIConfig(filepath.SplitList(cniPath), nil)
|
||||
|
||||
if b.bridgeName == "" {
|
||||
b.bridgeName = defaultNomadBridgeName
|
||||
}
|
||||
|
||||
if b.allocSubnet == "" {
|
||||
b.allocSubnet = defaultNomadAllocSubnet
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// Setup calls the CNI plugins with the add action
|
||||
func (b *bridgeNetworkConfigurator) Setup(alloc *structs.Allocation, spec *drivers.NetworkIsolationSpec) error {
|
||||
netconf, err := b.buildNomadNetConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := b.cniConfig.AddNetworkList(b.ctx, netconf, b.runtimeConf(alloc, spec))
|
||||
if result != nil {
|
||||
result.Print()
|
||||
}
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// Teardown calls the CNI plugins with the delete action
|
||||
func (b *bridgeNetworkConfigurator) Teardown(alloc *structs.Allocation, spec *drivers.NetworkIsolationSpec) error {
|
||||
netconf, err := b.buildNomadNetConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = b.cniConfig.DelNetworkList(b.ctx, netconf, b.runtimeConf(alloc, spec))
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// getPortMapping builds a list of portMapping structs that are used as the
|
||||
// portmapping capability arguments for the portmap CNI plugin
|
||||
func getPortMapping(alloc *structs.Allocation) []*portMapping {
|
||||
ports := []*portMapping{}
|
||||
for _, network := range alloc.AllocatedResources.Shared.Networks {
|
||||
for _, port := range append(network.DynamicPorts, network.ReservedPorts...) {
|
||||
for _, proto := range []string{"tcp", "udp"} {
|
||||
ports = append(ports, &portMapping{
|
||||
Host: port.Value,
|
||||
Container: port.To,
|
||||
Proto: proto,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return ports
|
||||
}
|
||||
|
||||
// portMapping is the json representation of the portmapping capability arguments
|
||||
// for the portmap CNI plugin
|
||||
type portMapping struct {
|
||||
Host int `json:"hostPort"`
|
||||
Container int `json:"containerPort"`
|
||||
Proto string `json:"protocol"`
|
||||
}
|
||||
|
||||
// runtimeConf builds the configuration needed by CNI to locate the target netns
|
||||
func (b *bridgeNetworkConfigurator) runtimeConf(alloc *structs.Allocation, spec *drivers.NetworkIsolationSpec) *libcni.RuntimeConf {
|
||||
return &libcni.RuntimeConf{
|
||||
ContainerID: fmt.Sprintf("nomad-%s", alloc.ID[:8]),
|
||||
NetNS: spec.Path,
|
||||
IfName: bridgeNetworkContainerIfName,
|
||||
CapabilityArgs: map[string]interface{}{
|
||||
"portMappings": getPortMapping(alloc),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// buildNomadNetConfig generates the CNI network configuration for the bridge
|
||||
// networking mode
|
||||
func (b *bridgeNetworkConfigurator) buildNomadNetConfig() (*libcni.NetworkConfigList, error) {
|
||||
rendered := fmt.Sprintf(nomadCNIConfigTemplate, b.bridgeName, b.allocSubnet)
|
||||
return libcni.ConfListFromBytes([]byte(rendered))
|
||||
}
|
||||
|
||||
const nomadCNIConfigTemplate = `{
|
||||
"cniVersion": "0.4.0",
|
||||
"name": "nomad",
|
||||
"plugins": [
|
||||
{
|
||||
"type": "bridge",
|
||||
"bridge": "%s",
|
||||
"isDefaultGateway": true,
|
||||
"ipMasq": true,
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"ranges": [
|
||||
[
|
||||
{
|
||||
"subnet": "%s"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "firewall"
|
||||
},
|
||||
{
|
||||
"type": "portmap",
|
||||
"capabilities": {"portMappings": true}
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
|
@ -202,6 +202,9 @@ type TaskRunner struct {
|
|||
// fails and the Run method should wait until serversContactedCh is
|
||||
// closed.
|
||||
waitOnServers bool
|
||||
|
||||
networkIsolationLock sync.Mutex
|
||||
networkIsolationSpec *drivers.NetworkIsolationSpec
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
|
@ -895,6 +898,8 @@ func (tr *TaskRunner) buildTaskConfig() *drivers.TaskConfig {
|
|||
invocationid := uuid.Generate()[:8]
|
||||
taskResources := tr.taskResources
|
||||
env := tr.envBuilder.Build()
|
||||
tr.networkIsolationLock.Lock()
|
||||
defer tr.networkIsolationLock.Unlock()
|
||||
|
||||
return &drivers.TaskConfig{
|
||||
ID: fmt.Sprintf("%s/%s/%s", alloc.ID, task.Name, invocationid),
|
||||
|
@ -918,6 +923,7 @@ func (tr *TaskRunner) buildTaskConfig() *drivers.TaskConfig {
|
|||
StdoutPath: tr.logmonHookConfig.stdoutFifo,
|
||||
StderrPath: tr.logmonHookConfig.stderrFifo,
|
||||
AllocID: tr.allocID,
|
||||
NetworkIsolation: tr.networkIsolationSpec,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1181,6 +1187,14 @@ func (tr *TaskRunner) Update(update *structs.Allocation) {
|
|||
}
|
||||
}
|
||||
|
||||
// SetNetworkIsolation is called by the PreRun allocation hook after configuring
|
||||
// the network isolation for the allocation
|
||||
func (tr *TaskRunner) SetNetworkIsolation(n *drivers.NetworkIsolationSpec) {
|
||||
tr.networkIsolationLock.Lock()
|
||||
tr.networkIsolationSpec = n
|
||||
tr.networkIsolationLock.Unlock()
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
|
|
@ -354,6 +354,18 @@ func NewClient(cfg *config.Config, consulCatalog consul.CatalogAPI, consulServic
|
|||
c.configCopy = c.config.Copy()
|
||||
c.configLock.Unlock()
|
||||
|
||||
// Auto download CNI binaries and configure CNI_PATH if requested.
|
||||
if c.config.AutoFetchCNI {
|
||||
if cniPath := FetchCNIPlugins(c.logger, c.config.AutoFetchCNIURL, c.config.AutoFetchCNIDir); cniPath != "" {
|
||||
if c.config.CNIPath == "" {
|
||||
c.config.CNIPath = cniPath
|
||||
} else {
|
||||
c.config.CNIPath = c.config.CNIPath + ":" + cniPath
|
||||
}
|
||||
c.logger.Debug("using new CNI Path", "cni_path", c.config.CNIPath)
|
||||
}
|
||||
}
|
||||
|
||||
fingerprintManager := NewFingerprintManager(
|
||||
c.configCopy.PluginSingletonLoader, c.GetConfig, c.configCopy.Node,
|
||||
c.shutdownCh, c.updateNodeFromFingerprint, c.logger)
|
||||
|
@ -556,6 +568,35 @@ func (c *Client) init() error {
|
|||
}
|
||||
|
||||
c.logger.Info("using alloc directory", "alloc_dir", c.config.AllocDir)
|
||||
|
||||
// Ensure the cnibin dir exists if we have one
|
||||
if c.config.AutoFetchCNIDir != "" {
|
||||
if err := os.MkdirAll(c.config.AutoFetchCNIDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory for AutoFetchCNIDir: %s", err)
|
||||
}
|
||||
} else {
|
||||
// Otherwise make a temp directory to use.
|
||||
p, err := ioutil.TempDir("", "NomadClient")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed creating temporary directory for the AutoFetchCNIDir: %v", err)
|
||||
}
|
||||
|
||||
p, err = filepath.EvalSymlinks(p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find temporary directory for the AutoFetchCNIDir: %v", err)
|
||||
}
|
||||
|
||||
// Change the permissions to have the execute bit
|
||||
if err := os.Chmod(p, 0755); err != nil {
|
||||
return fmt.Errorf("failed to change directory permissions for the AutoFetchCNIdir: %v", err)
|
||||
}
|
||||
|
||||
c.config.AutoFetchCNIDir = p
|
||||
}
|
||||
|
||||
if c.config.AutoFetchCNI {
|
||||
c.logger.Info("using cni directory for plugin downloads", "cni_dir", c.config.AutoFetchCNIDir)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
getter "github.com/hashicorp/go-getter"
|
||||
hclog "github.com/hashicorp/go-hclog"
|
||||
)
|
||||
|
||||
const (
|
||||
nomadCNIBinDir = "cnibin"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
// checksums are copied from https://github.com/containernetworking/plugins/releases
|
||||
defaultCNIGetterChecksums = map[string]string{
|
||||
"linux-amd64": "sha256:e9bfc78acd3ae71be77eb8f3e890cc9078a33cc3797703b8ff2fc3077a232252",
|
||||
"linux-arm": "sha256:ae6ddbd87c05a79aceb92e1c8c32d11e302f6fc55045f87f6a3ea7e0268b2fda",
|
||||
"linux-arm64": "sha256:acde854e3def3c776c532ae521c19d8784534918cc56449ff16945a2909bff6d",
|
||||
"windows-amd64": "sha256:a8a24e9cf93f4db92321afca3fe53bd3ccdf2b7117c403c55a5bac162d8d79cc",
|
||||
}
|
||||
defaultCNIPluginVersion = "0.8.1"
|
||||
defaultCNIGetterSrc = fmt.Sprintf("https://github.com/containernetworking/plugins/releases/download/v%s/cni-plugins-%s-%s-v%s.tgz?checksum=%s",
|
||||
defaultCNIPluginVersion, runtime.GOOS, runtime.GOARCH, defaultCNIPluginVersion,
|
||||
defaultCNIGetterChecksums[runtime.GOOS+"-"+runtime.GOARCH])
|
||||
)
|
||||
|
||||
// FetchCNIPlugins downloads the standard set of CNI plugins to the client's
|
||||
// data directory and returns the path to be used when setting up the CNI_PATH
|
||||
// environment variable. If an error occures during download, it is logged and
|
||||
// an empty path is returned
|
||||
func FetchCNIPlugins(logger hclog.Logger, src string, dataDir string) string {
|
||||
if src == "" {
|
||||
src = defaultCNIGetterSrc
|
||||
}
|
||||
|
||||
logger.Info("downloading CNI plugins", "url", src)
|
||||
dst := filepath.Join(dataDir, nomadCNIBinDir)
|
||||
if err := getter.Get(dst, src); err != nil {
|
||||
logger.Warn("failed to fetch CNI plugins", "url", src, "error", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return dst
|
||||
}
|
|
@ -221,6 +221,31 @@ type Config struct {
|
|||
|
||||
// StateDBFactory is used to override stateDB implementations,
|
||||
StateDBFactory state.NewStateDBFunc
|
||||
|
||||
// CNIPath is the path used to search for CNI plugins. Multiple paths can
|
||||
// be specified with colon delimited
|
||||
CNIPath string
|
||||
|
||||
// BridgeNetworkName is the name to use for the bridge created in bridge
|
||||
// networking mode. This defaults to 'nomad' if not set
|
||||
BridgeNetworkName string
|
||||
|
||||
// BridgeNetworkAllocSubnet is the IP subnet to use for address allocation
|
||||
// for allocations in bridge networking mode. Subnet must be in CIDR
|
||||
// notation
|
||||
BridgeNetworkAllocSubnet string
|
||||
|
||||
// AutoFetchCNI is a toggle to enable auto downloading of the CNI standard
|
||||
// plugins managed by the CNI team. This defaults to false
|
||||
AutoFetchCNI bool
|
||||
|
||||
// AutoFetchCNIURL is the go-getter URL to use when auto downloading CNI
|
||||
// plugins
|
||||
AutoFetchCNIURL string
|
||||
|
||||
// AutoFetchCNIDir is the destination dir to use when auto doanloading CNI plugins.
|
||||
// This directory will be appended to the CNIPath so it is searched last
|
||||
AutoFetchCNIDir string
|
||||
}
|
||||
|
||||
func (c *Config) Copy() *Config {
|
||||
|
@ -255,6 +280,7 @@ func DefaultConfig() *Config {
|
|||
DisableRemoteExec: false,
|
||||
BackwardsCompatibleMetrics: false,
|
||||
RPCHoldTimeout: 5 * time.Second,
|
||||
AutoFetchCNI: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
// Copyright 2018 CNI authors
|
||||
// Copyright 2019 HashiCorp
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// The functions in this file are derived from:
|
||||
// https://github.com/containernetworking/plugins/blob/0950a3607bf5e8a57c6a655c7e573e6aab0dc650/pkg/testutils/netns_linux.go
|
||||
|
||||
package nsutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// NetNSRunDir is the directory which new network namespaces will be bind mounted
|
||||
const NetNSRunDir = "/var/run/netns"
|
||||
|
||||
// NewNS creates a new persistent (bind-mounted) network namespace and returns
|
||||
// an object representing that namespace, without switching to it.
|
||||
func NewNS(nsName string) (ns.NetNS, error) {
|
||||
|
||||
// Create the directory for mounting network namespaces
|
||||
// This needs to be a shared mountpoint in case it is mounted in to
|
||||
// other namespaces (containers)
|
||||
err := os.MkdirAll(NetNSRunDir, 0755)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Remount the namespace directory shared. This will fail if it is not
|
||||
// already a mountpoint, so bind-mount it on to itself to "upgrade" it
|
||||
// to a mountpoint.
|
||||
err = unix.Mount("", NetNSRunDir, "none", unix.MS_SHARED|unix.MS_REC, "")
|
||||
if err != nil {
|
||||
if err != unix.EINVAL {
|
||||
return nil, fmt.Errorf("mount --make-rshared %s failed: %q", NetNSRunDir, err)
|
||||
}
|
||||
|
||||
// Recursively remount /var/run/netns on itself. The recursive flag is
|
||||
// so that any existing netns bindmounts are carried over.
|
||||
err = unix.Mount(NetNSRunDir, NetNSRunDir, "none", unix.MS_BIND|unix.MS_REC, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mount --rbind %s %s failed: %q", NetNSRunDir, NetNSRunDir, err)
|
||||
}
|
||||
|
||||
// Now we can make it shared
|
||||
err = unix.Mount("", NetNSRunDir, "none", unix.MS_SHARED|unix.MS_REC, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mount --make-rshared %s failed: %q", NetNSRunDir, err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// create an empty file at the mount point
|
||||
nsPath := path.Join(NetNSRunDir, nsName)
|
||||
mountPointFd, err := os.Create(nsPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mountPointFd.Close()
|
||||
|
||||
// Ensure the mount point is cleaned up on errors; if the namespace
|
||||
// was successfully mounted this will have no effect because the file
|
||||
// is in-use
|
||||
defer os.RemoveAll(nsPath)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
// do namespace work in a dedicated goroutine, so that we can safely
|
||||
// Lock/Unlock OSThread without upsetting the lock/unlock state of
|
||||
// the caller of this function
|
||||
go (func() {
|
||||
defer wg.Done()
|
||||
runtime.LockOSThread()
|
||||
// Don't unlock. By not unlocking, golang will kill the OS thread when the
|
||||
// goroutine is done (for go1.10+)
|
||||
|
||||
var origNS ns.NetNS
|
||||
origNS, err = ns.GetNS(getCurrentThreadNetNSPath())
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to get the current netns: %v", err)
|
||||
return
|
||||
}
|
||||
defer origNS.Close()
|
||||
|
||||
// create a new netns on the current thread
|
||||
err = unix.Unshare(unix.CLONE_NEWNET)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error from unshare: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Put this thread back to the orig ns, since it might get reused (pre go1.10)
|
||||
defer origNS.Set()
|
||||
|
||||
// bind mount the netns from the current thread (from /proc) onto the
|
||||
// mount point. This causes the namespace to persist, even when there
|
||||
// are no threads in the ns.
|
||||
err = unix.Mount(getCurrentThreadNetNSPath(), nsPath, "none", unix.MS_BIND, "")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to bind mount ns at %s: %v", nsPath, err)
|
||||
}
|
||||
})()
|
||||
wg.Wait()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create namespace: %v", err)
|
||||
}
|
||||
|
||||
return ns.GetNS(nsPath)
|
||||
}
|
||||
|
||||
// UnmountNS unmounts the NS held by the netns object
|
||||
func UnmountNS(nsPath string) error {
|
||||
// Only unmount if it's been bind-mounted (don't touch namespaces in /proc...)
|
||||
if strings.HasPrefix(nsPath, NetNSRunDir) {
|
||||
if err := unix.Unmount(nsPath, 0); err != nil {
|
||||
return fmt.Errorf("failed to unmount NS: at %s: %v", nsPath, err)
|
||||
}
|
||||
|
||||
if err := os.Remove(nsPath); err != nil {
|
||||
return fmt.Errorf("failed to remove ns path %s: %v", nsPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCurrentThreadNetNSPath copied from pkg/ns
|
||||
func getCurrentThreadNetNSPath() string {
|
||||
// /proc/self/ns/net returns the namespace of the main thread, not
|
||||
// of whatever thread this goroutine is running on. Make sure we
|
||||
// use the thread's net namespace since the thread is switching around
|
||||
return fmt.Sprintf("/proc/%d/task/%d/ns/net", os.Getpid(), unix.Gettid())
|
||||
}
|
|
@ -437,6 +437,7 @@ func convertClientConfig(agentConfig *Config) (*clientconfig.Config, error) {
|
|||
if agentConfig.DataDir != "" {
|
||||
conf.StateDir = filepath.Join(agentConfig.DataDir, "client")
|
||||
conf.AllocDir = filepath.Join(agentConfig.DataDir, "alloc")
|
||||
conf.AutoFetchCNIDir = filepath.Join(agentConfig.DataDir, "cnibin")
|
||||
}
|
||||
if agentConfig.Client.StateDir != "" {
|
||||
conf.StateDir = agentConfig.Client.StateDir
|
||||
|
@ -538,6 +539,13 @@ func convertClientConfig(agentConfig *Config) (*clientconfig.Config, error) {
|
|||
conf.ACLTokenTTL = agentConfig.ACL.TokenTTL
|
||||
conf.ACLPolicyTTL = agentConfig.ACL.PolicyTTL
|
||||
|
||||
// Setup networking configration
|
||||
conf.CNIPath = agentConfig.Client.CNIPath
|
||||
conf.BridgeNetworkName = agentConfig.Client.BridgeNetworkName
|
||||
conf.BridgeNetworkAllocSubnet = agentConfig.Client.BridgeNetworkSubnet
|
||||
conf.AutoFetchCNI = agentConfig.Client.AutoFetchCNIPlugins
|
||||
conf.AutoFetchCNIURL = agentConfig.Client.AutoFetchCNIPluginsURL
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -247,6 +247,29 @@ type ClientConfig struct {
|
|||
|
||||
// ExtraKeysHCL is used by hcl to surface unexpected keys
|
||||
ExtraKeysHCL []string `hcl:",unusedKeys" json:"-"`
|
||||
|
||||
// CNIPath is the path to search for CNI plugins, multiple paths can be
|
||||
// specified colon delimited
|
||||
CNIPath string `hcl:"cni_path"`
|
||||
|
||||
// BridgeNetworkName is the name of the bridge to create when using the
|
||||
// bridge network mode
|
||||
BridgeNetworkName string `hcl:"bridge_network_name"`
|
||||
|
||||
// BridgeNetworkSubnet is the subnet to allocate IP addresses from when
|
||||
// creating allocations with bridge networking mode. This range is local to
|
||||
// the host
|
||||
BridgeNetworkSubnet string `hcl:"bridge_network_subnet"`
|
||||
|
||||
// AutoFetchCNIPlugins toggles if the Nomad client should attempt to
|
||||
// automatically download a standard set of CNI plugins, typically from
|
||||
// the community repo https://github.com/containernetworking/plugins/releases
|
||||
AutoFetchCNIPlugins bool `hcl:"auto_fetch_cni_plugins"`
|
||||
|
||||
// AutoFetchCNIPluginsURL sets the source URL to be used if automatically
|
||||
// downloading CNI plugins. If not set will use a known working version from
|
||||
// the community repo https://github.com/containernetworking/plugins/releases
|
||||
AutoFetchCNIPluginsURL string `hcl:"auto_fetch_cni_plugins_url"`
|
||||
}
|
||||
|
||||
// ACLConfig is configuration specific to the ACL system
|
||||
|
@ -661,6 +684,7 @@ func DevConfig() *Config {
|
|||
conf.Telemetry.PrometheusMetrics = true
|
||||
conf.Telemetry.PublishAllocationMetrics = true
|
||||
conf.Telemetry.PublishNodeMetrics = true
|
||||
conf.Client.AutoFetchCNIPlugins = true
|
||||
|
||||
return conf
|
||||
}
|
||||
|
|
|
@ -685,6 +685,8 @@ func ApiTgToStructsTG(taskGroup *api.TaskGroup, tg *structs.TaskGroup) {
|
|||
tg.Meta = taskGroup.Meta
|
||||
tg.Constraints = ApiConstraintsToStructs(taskGroup.Constraints)
|
||||
tg.Affinities = ApiAffinitiesToStructs(taskGroup.Affinities)
|
||||
tg.Networks = ApiNetworkResourceToStructs(taskGroup.Networks)
|
||||
tg.Services = ApiServicesToStructs(taskGroup.Services)
|
||||
|
||||
tg.RestartPolicy = &structs.RestartPolicy{
|
||||
Attempts: *taskGroup.RestartPolicy.Attempts,
|
||||
|
@ -886,35 +888,8 @@ func ApiResourcesToStructs(in *api.Resources) *structs.Resources {
|
|||
out.IOPS = *in.IOPS
|
||||
}
|
||||
|
||||
if l := len(in.Networks); l != 0 {
|
||||
out.Networks = make([]*structs.NetworkResource, l)
|
||||
for i, nw := range in.Networks {
|
||||
out.Networks[i] = &structs.NetworkResource{
|
||||
CIDR: nw.CIDR,
|
||||
IP: nw.IP,
|
||||
MBits: *nw.MBits,
|
||||
}
|
||||
|
||||
if l := len(nw.DynamicPorts); l != 0 {
|
||||
out.Networks[i].DynamicPorts = make([]structs.Port, l)
|
||||
for j, dp := range nw.DynamicPorts {
|
||||
out.Networks[i].DynamicPorts[j] = structs.Port{
|
||||
Label: dp.Label,
|
||||
Value: dp.Value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if l := len(nw.ReservedPorts); l != 0 {
|
||||
out.Networks[i].ReservedPorts = make([]structs.Port, l)
|
||||
for j, rp := range nw.ReservedPorts {
|
||||
out.Networks[i].ReservedPorts[j] = structs.Port{
|
||||
Label: rp.Label,
|
||||
Value: rp.Value,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(in.Networks) != 0 {
|
||||
out.Networks = ApiNetworkResourceToStructs(in.Networks)
|
||||
}
|
||||
|
||||
if l := len(in.Devices); l != 0 {
|
||||
|
@ -932,6 +907,127 @@ func ApiResourcesToStructs(in *api.Resources) *structs.Resources {
|
|||
return out
|
||||
}
|
||||
|
||||
func ApiNetworkResourceToStructs(in []*api.NetworkResource) []*structs.NetworkResource {
|
||||
var out []*structs.NetworkResource
|
||||
if len(in) == 0 {
|
||||
return out
|
||||
}
|
||||
out = make([]*structs.NetworkResource, len(in))
|
||||
for i, nw := range in {
|
||||
out[i] = &structs.NetworkResource{
|
||||
Mode: nw.Mode,
|
||||
CIDR: nw.CIDR,
|
||||
IP: nw.IP,
|
||||
MBits: *nw.MBits,
|
||||
}
|
||||
|
||||
if l := len(nw.DynamicPorts); l != 0 {
|
||||
out[i].DynamicPorts = make([]structs.Port, l)
|
||||
for j, dp := range nw.DynamicPorts {
|
||||
out[i].DynamicPorts[j] = structs.Port{
|
||||
Label: dp.Label,
|
||||
Value: dp.Value,
|
||||
To: dp.To,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if l := len(nw.ReservedPorts); l != 0 {
|
||||
out[i].ReservedPorts = make([]structs.Port, l)
|
||||
for j, rp := range nw.ReservedPorts {
|
||||
out[i].ReservedPorts[j] = structs.Port{
|
||||
Label: rp.Label,
|
||||
Value: rp.Value,
|
||||
To: rp.To,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
//TODO(schmichael) refactor and reuse in service parsing above
|
||||
func ApiServicesToStructs(in []*api.Service) []*structs.Service {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]*structs.Service, len(in))
|
||||
for i, s := range in {
|
||||
out[i] = &structs.Service{
|
||||
Name: s.Name,
|
||||
PortLabel: s.PortLabel,
|
||||
Tags: s.Tags,
|
||||
CanaryTags: s.CanaryTags,
|
||||
AddressMode: s.AddressMode,
|
||||
}
|
||||
|
||||
if l := len(s.Checks); l != 0 {
|
||||
out[i].Checks = make([]*structs.ServiceCheck, l)
|
||||
for j, check := range s.Checks {
|
||||
out[i].Checks[j] = &structs.ServiceCheck{
|
||||
Name: check.Name,
|
||||
Type: check.Type,
|
||||
Command: check.Command,
|
||||
Args: check.Args,
|
||||
Path: check.Path,
|
||||
Protocol: check.Protocol,
|
||||
PortLabel: check.PortLabel,
|
||||
AddressMode: check.AddressMode,
|
||||
Interval: check.Interval,
|
||||
Timeout: check.Timeout,
|
||||
InitialStatus: check.InitialStatus,
|
||||
TLSSkipVerify: check.TLSSkipVerify,
|
||||
Header: check.Header,
|
||||
Method: check.Method,
|
||||
GRPCService: check.GRPCService,
|
||||
GRPCUseTLS: check.GRPCUseTLS,
|
||||
}
|
||||
if check.CheckRestart != nil {
|
||||
out[i].Checks[j].CheckRestart = &structs.CheckRestart{
|
||||
Limit: check.CheckRestart.Limit,
|
||||
Grace: *check.CheckRestart.Grace,
|
||||
IgnoreWarnings: check.CheckRestart.IgnoreWarnings,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if s.Connect == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
out[i].Connect = &structs.ConsulConnect{}
|
||||
|
||||
if s.Connect.SidecarService == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
out[i].Connect.SidecarService = &structs.ConsulSidecarService{
|
||||
Port: s.Connect.SidecarService.Port,
|
||||
}
|
||||
|
||||
if s.Connect.SidecarService.Proxy == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
out[i].Connect.SidecarService.Proxy = &structs.ConsulProxy{}
|
||||
|
||||
upstreams := make([]*structs.ConsulUpstream, len(s.Connect.SidecarService.Proxy.Upstreams))
|
||||
for i, p := range s.Connect.SidecarService.Proxy.Upstreams {
|
||||
upstreams[i] = &structs.ConsulUpstream{
|
||||
DestinationName: p.DestinationName,
|
||||
LocalBindPort: p.LocalBindPort,
|
||||
}
|
||||
}
|
||||
|
||||
out[i].Connect.SidecarService.Proxy.Upstreams = upstreams
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func ApiConstraintsToStructs(in []*api.Constraint) []*structs.Constraint {
|
||||
if in == nil {
|
||||
return nil
|
||||
|
|
|
@ -216,6 +216,12 @@ var (
|
|||
hclspec.NewAttr("nvidia_runtime", "string", false),
|
||||
hclspec.NewLiteral(`"nvidia"`),
|
||||
),
|
||||
|
||||
// image to use when creating a network namespace parent container
|
||||
"infra_image": hclspec.NewDefault(
|
||||
hclspec.NewAttr("infra_image", "string", false),
|
||||
hclspec.NewLiteral(`"gcr.io/google_containers/pause-amd64:3.0"`),
|
||||
),
|
||||
})
|
||||
|
||||
// taskConfigSpec is the hcl specification for the driver config section of
|
||||
|
@ -310,6 +316,12 @@ var (
|
|||
SendSignals: true,
|
||||
Exec: true,
|
||||
FSIsolation: drivers.FSIsolationImage,
|
||||
NetIsolationModes: []drivers.NetIsolationMode{
|
||||
drivers.NetIsolationModeHost,
|
||||
drivers.NetIsolationModeGroup,
|
||||
drivers.NetIsolationModeTask,
|
||||
},
|
||||
MustInitiateNetwork: true,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -485,6 +497,7 @@ type DriverConfig struct {
|
|||
AllowPrivileged bool `codec:"allow_privileged"`
|
||||
AllowCaps []string `codec:"allow_caps"`
|
||||
GPURuntimeName string `codec:"nvidia_runtime"`
|
||||
InfraImage string `codec:"infra_image"`
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
|
|
|
@ -65,6 +65,9 @@ type DockerImageClient interface {
|
|||
// LogEventFn is a callback which allows Drivers to emit task events.
|
||||
type LogEventFn func(message string, annotations map[string]string)
|
||||
|
||||
// noopLogEventFn satisfies the LogEventFn type but noops when called
|
||||
func noopLogEventFn(string, map[string]string) {}
|
||||
|
||||
// dockerCoordinatorConfig is used to configure the Docker coordinator.
|
||||
type dockerCoordinatorConfig struct {
|
||||
// logger is the logger the coordinator should use
|
||||
|
|
|
@ -266,7 +266,7 @@ func (d *Driver) StartTask(cfg *drivers.TaskConfig) (*drivers.TaskHandle, *drive
|
|||
|
||||
startAttempts := 0
|
||||
CREATE:
|
||||
container, err := d.createContainer(client, containerCfg, &driverConfig)
|
||||
container, err := d.createContainer(client, containerCfg, driverConfig.Image)
|
||||
if err != nil {
|
||||
d.logger.Error("failed to create container", "error", err)
|
||||
return nil, nil, nstructs.WrapRecoverable(fmt.Sprintf("failed to create container: %v", err), err)
|
||||
|
@ -368,7 +368,7 @@ type createContainerClient interface {
|
|||
// createContainer creates the container given the passed configuration. It
|
||||
// attempts to handle any transient Docker errors.
|
||||
func (d *Driver) createContainer(client createContainerClient, config docker.CreateContainerOptions,
|
||||
driverConfig *TaskConfig) (*docker.Container, error) {
|
||||
image string) (*docker.Container, error) {
|
||||
// Create a container
|
||||
attempted := 0
|
||||
CREATE:
|
||||
|
@ -378,7 +378,7 @@ CREATE:
|
|||
}
|
||||
|
||||
d.logger.Debug("failed to create container", "container_name",
|
||||
config.Name, "image_name", driverConfig.Image, "image_id", config.Config.Image,
|
||||
config.Name, "image_name", image, "image_id", config.Config.Image,
|
||||
"attempt", attempted+1, "error", createErr)
|
||||
|
||||
// Volume management tools like Portworx may not have detached a volume
|
||||
|
@ -869,12 +869,23 @@ func (d *Driver) createContainerConfig(task *drivers.TaskConfig, driverConfig *T
|
|||
|
||||
hostConfig.ReadonlyRootfs = driverConfig.ReadonlyRootfs
|
||||
|
||||
// set the docker network mode
|
||||
hostConfig.NetworkMode = driverConfig.NetworkMode
|
||||
|
||||
// if the driver config does not specify a network mode then try to use the
|
||||
// shared alloc network
|
||||
if hostConfig.NetworkMode == "" {
|
||||
if task.NetworkIsolation != nil && task.NetworkIsolation.Path != "" {
|
||||
// find the previously created parent container to join networks with
|
||||
netMode := fmt.Sprintf("container:%s", task.NetworkIsolation.Labels[dockerNetSpecLabelKey])
|
||||
logger.Debug("configuring network mode for task group", "network_mode", netMode)
|
||||
hostConfig.NetworkMode = netMode
|
||||
} else {
|
||||
// docker default
|
||||
logger.Debug("networking mode not specified; using default", "network_mode", defaultNetworkMode)
|
||||
hostConfig.NetworkMode = defaultNetworkMode
|
||||
}
|
||||
}
|
||||
|
||||
// Setup port mapping and exposed ports
|
||||
if len(task.Resources.NomadResources.Networks) == 0 {
|
||||
|
|
|
@ -2230,7 +2230,7 @@ func TestDockerDriver_VolumeError(t *testing.T) {
|
|||
driver := dockerDriverHarness(t, nil)
|
||||
|
||||
// assert volume error is recoverable
|
||||
_, err := driver.Impl().(*Driver).createContainer(fakeDockerClient{}, docker.CreateContainerOptions{Config: &docker.Config{}}, cfg)
|
||||
_, err := driver.Impl().(*Driver).createContainer(fakeDockerClient{}, docker.CreateContainerOptions{Config: &docker.Config{}}, cfg.Image)
|
||||
require.True(t, structs.IsRecoverable(err))
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
docker "github.com/fsouza/go-dockerclient"
|
||||
"github.com/hashicorp/nomad/plugins/drivers"
|
||||
)
|
||||
|
||||
// dockerNetSpecLabelKey is used when creating a parent container for
|
||||
// shared networking. It is a label whos value identifies the container ID of
|
||||
// the parent container so tasks can configure their network mode accordingly
|
||||
const dockerNetSpecLabelKey = "docker_sandbox_container_id"
|
||||
|
||||
func (d *Driver) CreateNetwork(allocID string) (*drivers.NetworkIsolationSpec, error) {
|
||||
// Initialize docker API clients
|
||||
client, _, err := d.dockerClients()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to docker daemon: %s", err)
|
||||
}
|
||||
|
||||
repo, _ := parseDockerImage(d.config.InfraImage)
|
||||
authOptions, err := firstValidAuth(repo, []authBackend{
|
||||
authFromDockerConfig(d.config.Auth.Config),
|
||||
authFromHelper(d.config.Auth.Helper),
|
||||
})
|
||||
if err != nil {
|
||||
d.logger.Debug("auth failed for infra container image pull", "image", d.config.InfraImage, "error", err)
|
||||
}
|
||||
_, err = d.coordinator.PullImage(d.config.InfraImage, authOptions, allocID, noopLogEventFn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := d.createSandboxContainerConfig(allocID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
container, err := d.createContainer(client, *config, d.config.InfraImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := d.startContainer(container); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c, err := client.InspectContainer(container.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &drivers.NetworkIsolationSpec{
|
||||
Mode: drivers.NetIsolationModeGroup,
|
||||
Path: c.NetworkSettings.SandboxKey,
|
||||
Labels: map[string]string{
|
||||
dockerNetSpecLabelKey: c.ID,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Driver) DestroyNetwork(allocID string, spec *drivers.NetworkIsolationSpec) error {
|
||||
client, _, err := d.dockerClients()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to docker daemon: %s", err)
|
||||
}
|
||||
|
||||
return client.RemoveContainer(docker.RemoveContainerOptions{
|
||||
Force: true,
|
||||
ID: spec.Labels[dockerNetSpecLabelKey],
|
||||
})
|
||||
}
|
||||
|
||||
// createSandboxContainerConfig creates a docker container configuration which
|
||||
// starts a container with an empty network namespace
|
||||
func (d *Driver) createSandboxContainerConfig(allocID string) (*docker.CreateContainerOptions, error) {
|
||||
|
||||
return &docker.CreateContainerOptions{
|
||||
Name: fmt.Sprintf("nomad_init_%s", allocID),
|
||||
Config: &docker.Config{
|
||||
Image: d.config.InfraImage,
|
||||
},
|
||||
HostConfig: &docker.HostConfig{
|
||||
// set the network mode to none which creates a network namespace with
|
||||
// only a loopback interface
|
||||
NetworkMode: "none",
|
||||
},
|
||||
}, nil
|
||||
}
|
|
@ -353,6 +353,7 @@ func (d *Driver) StartTask(cfg *drivers.TaskConfig) (*drivers.TaskHandle, *drive
|
|||
StderrPath: cfg.StderrPath,
|
||||
Mounts: cfg.Mounts,
|
||||
Devices: cfg.Devices,
|
||||
NetworkIsolation: cfg.NetworkIsolation,
|
||||
}
|
||||
|
||||
ps, err := exec.Launch(execCmd)
|
||||
|
|
|
@ -678,3 +678,11 @@ func (d *Driver) GetHandle(taskID string) *taskHandle {
|
|||
func (d *Driver) Shutdown() {
|
||||
d.signalShutdown()
|
||||
}
|
||||
|
||||
func (d *Driver) CreateNetwork(allocID string) (*drivers.NetworkIsolationSpec, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (d *Driver) DestroyNetwork(allocID string, spec *drivers.NetworkIsolationSpec) error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -95,6 +95,10 @@ var (
|
|||
SendSignals: true,
|
||||
Exec: true,
|
||||
FSIsolation: drivers.FSIsolationNone,
|
||||
NetIsolationModes: []drivers.NetIsolationMode{
|
||||
drivers.NetIsolationModeHost,
|
||||
drivers.NetIsolationModeGroup,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -342,6 +346,7 @@ func (d *Driver) StartTask(cfg *drivers.TaskConfig) (*drivers.TaskHandle, *drive
|
|||
TaskDir: cfg.TaskDir().Dir,
|
||||
StdoutPath: cfg.StdoutPath,
|
||||
StderrPath: cfg.StderrPath,
|
||||
NetworkIsolation: cfg.NetworkIsolation,
|
||||
}
|
||||
|
||||
ps, err := exec.Launch(execCmd)
|
||||
|
|
|
@ -43,6 +43,7 @@ func (c *grpcExecutorClient) Launch(cmd *ExecCommand) (*ProcessState, error) {
|
|||
BasicProcessCgroup: cmd.BasicProcessCgroup,
|
||||
Mounts: drivers.MountsToProto(cmd.Mounts),
|
||||
Devices: drivers.DevicesToProto(cmd.Devices),
|
||||
NetworkIsolation: drivers.NetworkIsolationSpecToProto(cmd.NetworkIsolation),
|
||||
}
|
||||
resp, err := c.client.Launch(ctx, req)
|
||||
if err != nil {
|
||||
|
|
|
@ -126,6 +126,8 @@ type ExecCommand struct {
|
|||
|
||||
// Devices are the the device nodes to be created in isolation environment
|
||||
Devices []*drivers.DeviceConfig
|
||||
|
||||
NetworkIsolation *drivers.NetworkIsolationSpec
|
||||
}
|
||||
|
||||
// SetWriters sets the writer for the process stdout and stderr. This should
|
||||
|
@ -307,8 +309,7 @@ func (e *UniversalExecutor) Launch(command *ExecCommand) (*ProcessState, error)
|
|||
e.childCmd.Env = e.commandCfg.Env
|
||||
|
||||
// Start the process
|
||||
e.logger.Debug("launching", "command", command.Cmd, "args", strings.Join(command.Args, " "))
|
||||
if err := e.childCmd.Start(); err != nil {
|
||||
if err = e.start(command); err != nil {
|
||||
return nil, fmt.Errorf("failed to start command path=%q --- args=%q: %v", path, e.childCmd.Args, err)
|
||||
}
|
||||
|
||||
|
|
|
@ -19,3 +19,7 @@ func (e *UniversalExecutor) runAs(_ string) error { return nil }
|
|||
func (e *UniversalExecutor) getAllPids() (map[int]*nomadPid, error) {
|
||||
return getAllPidsByScanning()
|
||||
}
|
||||
|
||||
func (e *UniversalExecutor) start(command *ExecCommand) error {
|
||||
return e.childCmd.Start()
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/armon/circbuf"
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
"github.com/hashicorp/consul-template/signals"
|
||||
hclog "github.com/hashicorp/go-hclog"
|
||||
multierror "github.com/hashicorp/go-multierror"
|
||||
|
@ -183,10 +184,24 @@ func (l *LibcontainerExecutor) Launch(command *ExecCommand) (*ProcessState, erro
|
|||
l.systemCpuStats = stats.NewCpuStats()
|
||||
|
||||
// Starts the task
|
||||
if command.NetworkIsolation != nil && command.NetworkIsolation.Path != "" {
|
||||
netns, err := ns.GetNS(command.NetworkIsolation.Path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ns %s: %v", command.NetworkIsolation.Path, err)
|
||||
}
|
||||
|
||||
// Start the container in the network namespace
|
||||
err = netns.Do(func(ns.NetNS) error { return container.Run(process) })
|
||||
if err != nil {
|
||||
container.Destroy()
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if err := container.Run(process); err != nil {
|
||||
container.Destroy()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
pid, err := process.Pid()
|
||||
if err != nil {
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/containernetworking/plugins/pkg/ns"
|
||||
multierror "github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/nomad/helper"
|
||||
"github.com/opencontainers/runc/libcontainer/cgroups"
|
||||
|
@ -171,3 +172,20 @@ func DestroyCgroup(groups *lconfigs.Cgroup, executorPid int) error {
|
|||
}
|
||||
return mErrs.ErrorOrNil()
|
||||
}
|
||||
|
||||
func (e *UniversalExecutor) start(command *ExecCommand) error {
|
||||
if command.NetworkIsolation != nil && command.NetworkIsolation.Path != "" {
|
||||
// Get a handle to the target network namespace
|
||||
netns, err := ns.GetNS(command.NetworkIsolation.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start the container in the network namespace
|
||||
return netns.Do(func(ns.NetNS) error {
|
||||
return e.childCmd.Start()
|
||||
})
|
||||
}
|
||||
|
||||
return e.childCmd.Start()
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ type LaunchRequest struct {
|
|||
BasicProcessCgroup bool `protobuf:"varint,10,opt,name=basic_process_cgroup,json=basicProcessCgroup,proto3" json:"basic_process_cgroup,omitempty"`
|
||||
Mounts []*proto1.Mount `protobuf:"bytes,11,rep,name=mounts,proto3" json:"mounts,omitempty"`
|
||||
Devices []*proto1.Device `protobuf:"bytes,12,rep,name=devices,proto3" json:"devices,omitempty"`
|
||||
NetworkIsolation *proto1.NetworkIsolationSpec `protobuf:"bytes,13,opt,name=network_isolation,json=networkIsolation,proto3" json:"network_isolation,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
|
@ -47,7 +48,7 @@ func (m *LaunchRequest) Reset() { *m = LaunchRequest{} }
|
|||
func (m *LaunchRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*LaunchRequest) ProtoMessage() {}
|
||||
func (*LaunchRequest) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_executor_5ea6ca9df3b0f07e, []int{0}
|
||||
return fileDescriptor_executor_43dc81e71868eb7b, []int{0}
|
||||
}
|
||||
func (m *LaunchRequest) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_LaunchRequest.Unmarshal(m, b)
|
||||
|
@ -151,6 +152,13 @@ func (m *LaunchRequest) GetDevices() []*proto1.Device {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *LaunchRequest) GetNetworkIsolation() *proto1.NetworkIsolationSpec {
|
||||
if m != nil {
|
||||
return m.NetworkIsolation
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type LaunchResponse struct {
|
||||
Process *ProcessState `protobuf:"bytes,1,opt,name=process,proto3" json:"process,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
|
@ -162,7 +170,7 @@ func (m *LaunchResponse) Reset() { *m = LaunchResponse{} }
|
|||
func (m *LaunchResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*LaunchResponse) ProtoMessage() {}
|
||||
func (*LaunchResponse) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_executor_5ea6ca9df3b0f07e, []int{1}
|
||||
return fileDescriptor_executor_43dc81e71868eb7b, []int{1}
|
||||
}
|
||||
func (m *LaunchResponse) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_LaunchResponse.Unmarshal(m, b)
|
||||
|
@ -199,7 +207,7 @@ func (m *WaitRequest) Reset() { *m = WaitRequest{} }
|
|||
func (m *WaitRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*WaitRequest) ProtoMessage() {}
|
||||
func (*WaitRequest) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_executor_5ea6ca9df3b0f07e, []int{2}
|
||||
return fileDescriptor_executor_43dc81e71868eb7b, []int{2}
|
||||
}
|
||||
func (m *WaitRequest) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_WaitRequest.Unmarshal(m, b)
|
||||
|
@ -230,7 +238,7 @@ func (m *WaitResponse) Reset() { *m = WaitResponse{} }
|
|||
func (m *WaitResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*WaitResponse) ProtoMessage() {}
|
||||
func (*WaitResponse) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_executor_5ea6ca9df3b0f07e, []int{3}
|
||||
return fileDescriptor_executor_43dc81e71868eb7b, []int{3}
|
||||
}
|
||||
func (m *WaitResponse) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_WaitResponse.Unmarshal(m, b)
|
||||
|
@ -269,7 +277,7 @@ func (m *ShutdownRequest) Reset() { *m = ShutdownRequest{} }
|
|||
func (m *ShutdownRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*ShutdownRequest) ProtoMessage() {}
|
||||
func (*ShutdownRequest) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_executor_5ea6ca9df3b0f07e, []int{4}
|
||||
return fileDescriptor_executor_43dc81e71868eb7b, []int{4}
|
||||
}
|
||||
func (m *ShutdownRequest) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ShutdownRequest.Unmarshal(m, b)
|
||||
|
@ -313,7 +321,7 @@ func (m *ShutdownResponse) Reset() { *m = ShutdownResponse{} }
|
|||
func (m *ShutdownResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*ShutdownResponse) ProtoMessage() {}
|
||||
func (*ShutdownResponse) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_executor_5ea6ca9df3b0f07e, []int{5}
|
||||
return fileDescriptor_executor_43dc81e71868eb7b, []int{5}
|
||||
}
|
||||
func (m *ShutdownResponse) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ShutdownResponse.Unmarshal(m, b)
|
||||
|
@ -344,7 +352,7 @@ func (m *UpdateResourcesRequest) Reset() { *m = UpdateResourcesRequest{}
|
|||
func (m *UpdateResourcesRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*UpdateResourcesRequest) ProtoMessage() {}
|
||||
func (*UpdateResourcesRequest) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_executor_5ea6ca9df3b0f07e, []int{6}
|
||||
return fileDescriptor_executor_43dc81e71868eb7b, []int{6}
|
||||
}
|
||||
func (m *UpdateResourcesRequest) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_UpdateResourcesRequest.Unmarshal(m, b)
|
||||
|
@ -381,7 +389,7 @@ func (m *UpdateResourcesResponse) Reset() { *m = UpdateResourcesResponse
|
|||
func (m *UpdateResourcesResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*UpdateResourcesResponse) ProtoMessage() {}
|
||||
func (*UpdateResourcesResponse) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_executor_5ea6ca9df3b0f07e, []int{7}
|
||||
return fileDescriptor_executor_43dc81e71868eb7b, []int{7}
|
||||
}
|
||||
func (m *UpdateResourcesResponse) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_UpdateResourcesResponse.Unmarshal(m, b)
|
||||
|
@ -411,7 +419,7 @@ func (m *VersionRequest) Reset() { *m = VersionRequest{} }
|
|||
func (m *VersionRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*VersionRequest) ProtoMessage() {}
|
||||
func (*VersionRequest) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_executor_5ea6ca9df3b0f07e, []int{8}
|
||||
return fileDescriptor_executor_43dc81e71868eb7b, []int{8}
|
||||
}
|
||||
func (m *VersionRequest) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_VersionRequest.Unmarshal(m, b)
|
||||
|
@ -442,7 +450,7 @@ func (m *VersionResponse) Reset() { *m = VersionResponse{} }
|
|||
func (m *VersionResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*VersionResponse) ProtoMessage() {}
|
||||
func (*VersionResponse) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_executor_5ea6ca9df3b0f07e, []int{9}
|
||||
return fileDescriptor_executor_43dc81e71868eb7b, []int{9}
|
||||
}
|
||||
func (m *VersionResponse) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_VersionResponse.Unmarshal(m, b)
|
||||
|
@ -480,7 +488,7 @@ func (m *StatsRequest) Reset() { *m = StatsRequest{} }
|
|||
func (m *StatsRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*StatsRequest) ProtoMessage() {}
|
||||
func (*StatsRequest) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_executor_5ea6ca9df3b0f07e, []int{10}
|
||||
return fileDescriptor_executor_43dc81e71868eb7b, []int{10}
|
||||
}
|
||||
func (m *StatsRequest) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_StatsRequest.Unmarshal(m, b)
|
||||
|
@ -518,7 +526,7 @@ func (m *StatsResponse) Reset() { *m = StatsResponse{} }
|
|||
func (m *StatsResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*StatsResponse) ProtoMessage() {}
|
||||
func (*StatsResponse) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_executor_5ea6ca9df3b0f07e, []int{11}
|
||||
return fileDescriptor_executor_43dc81e71868eb7b, []int{11}
|
||||
}
|
||||
func (m *StatsResponse) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_StatsResponse.Unmarshal(m, b)
|
||||
|
@ -556,7 +564,7 @@ func (m *SignalRequest) Reset() { *m = SignalRequest{} }
|
|||
func (m *SignalRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*SignalRequest) ProtoMessage() {}
|
||||
func (*SignalRequest) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_executor_5ea6ca9df3b0f07e, []int{12}
|
||||
return fileDescriptor_executor_43dc81e71868eb7b, []int{12}
|
||||
}
|
||||
func (m *SignalRequest) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_SignalRequest.Unmarshal(m, b)
|
||||
|
@ -593,7 +601,7 @@ func (m *SignalResponse) Reset() { *m = SignalResponse{} }
|
|||
func (m *SignalResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*SignalResponse) ProtoMessage() {}
|
||||
func (*SignalResponse) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_executor_5ea6ca9df3b0f07e, []int{13}
|
||||
return fileDescriptor_executor_43dc81e71868eb7b, []int{13}
|
||||
}
|
||||
func (m *SignalResponse) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_SignalResponse.Unmarshal(m, b)
|
||||
|
@ -626,7 +634,7 @@ func (m *ExecRequest) Reset() { *m = ExecRequest{} }
|
|||
func (m *ExecRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*ExecRequest) ProtoMessage() {}
|
||||
func (*ExecRequest) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_executor_5ea6ca9df3b0f07e, []int{14}
|
||||
return fileDescriptor_executor_43dc81e71868eb7b, []int{14}
|
||||
}
|
||||
func (m *ExecRequest) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ExecRequest.Unmarshal(m, b)
|
||||
|
@ -679,7 +687,7 @@ func (m *ExecResponse) Reset() { *m = ExecResponse{} }
|
|||
func (m *ExecResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*ExecResponse) ProtoMessage() {}
|
||||
func (*ExecResponse) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_executor_5ea6ca9df3b0f07e, []int{15}
|
||||
return fileDescriptor_executor_43dc81e71868eb7b, []int{15}
|
||||
}
|
||||
func (m *ExecResponse) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ExecResponse.Unmarshal(m, b)
|
||||
|
@ -727,7 +735,7 @@ func (m *ProcessState) Reset() { *m = ProcessState{} }
|
|||
func (m *ProcessState) String() string { return proto.CompactTextString(m) }
|
||||
func (*ProcessState) ProtoMessage() {}
|
||||
func (*ProcessState) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_executor_5ea6ca9df3b0f07e, []int{16}
|
||||
return fileDescriptor_executor_43dc81e71868eb7b, []int{16}
|
||||
}
|
||||
func (m *ProcessState) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_ProcessState.Unmarshal(m, b)
|
||||
|
@ -1192,67 +1200,69 @@ var _Executor_serviceDesc = grpc.ServiceDesc{
|
|||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterFile("drivers/shared/executor/proto/executor.proto", fileDescriptor_executor_5ea6ca9df3b0f07e)
|
||||
proto.RegisterFile("drivers/shared/executor/proto/executor.proto", fileDescriptor_executor_43dc81e71868eb7b)
|
||||
}
|
||||
|
||||
var fileDescriptor_executor_5ea6ca9df3b0f07e = []byte{
|
||||
// 919 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0x5f, 0x6f, 0xdc, 0x44,
|
||||
0x10, 0xaf, 0xeb, 0xdc, 0xbf, 0xb9, 0xbb, 0x24, 0x5a, 0xa1, 0xe0, 0x9a, 0x87, 0x1e, 0x7e, 0xa0,
|
||||
0x27, 0x28, 0xbe, 0x28, 0xfd, 0xc7, 0x0b, 0x14, 0x91, 0x14, 0x5e, 0x42, 0x15, 0x39, 0x85, 0x4a,
|
||||
0x3c, 0x70, 0x6c, 0xec, 0xc5, 0x5e, 0xe5, 0xce, 0x6b, 0x76, 0xd7, 0x47, 0x90, 0x90, 0x78, 0xe2,
|
||||
0x1b, 0x80, 0xc4, 0xe7, 0xe4, 0x13, 0xa0, 0xfd, 0xe7, 0xdc, 0xa5, 0xa5, 0xf2, 0x15, 0xf1, 0x74,
|
||||
0x3b, 0xe3, 0xf9, 0xfd, 0x66, 0x66, 0x77, 0xe6, 0x77, 0x70, 0x3f, 0xe3, 0x74, 0x45, 0xb8, 0x98,
|
||||
0x89, 0x02, 0x73, 0x92, 0xcd, 0xc8, 0x15, 0x49, 0x6b, 0xc9, 0xf8, 0xac, 0xe2, 0x4c, 0xb2, 0xc6,
|
||||
0x8c, 0xb5, 0x89, 0x3e, 0x28, 0xb0, 0x28, 0x68, 0xca, 0x78, 0x15, 0x97, 0x6c, 0x89, 0xb3, 0xb8,
|
||||
0x5a, 0xd4, 0x39, 0x2d, 0x45, 0xbc, 0x19, 0x17, 0xde, 0xcd, 0x19, 0xcb, 0x17, 0xc4, 0x90, 0x5c,
|
||||
0xd4, 0x3f, 0xce, 0x24, 0x5d, 0x12, 0x21, 0xf1, 0xb2, 0xb2, 0x01, 0x9f, 0xe6, 0x54, 0x16, 0xf5,
|
||||
0x45, 0x9c, 0xb2, 0xe5, 0xac, 0xe1, 0x9c, 0x69, 0xce, 0x99, 0xe5, 0x9c, 0xb9, 0xca, 0x4c, 0x25,
|
||||
0xc6, 0x32, 0xf0, 0xe8, 0x6f, 0x1f, 0xc6, 0xa7, 0xb8, 0x2e, 0xd3, 0x22, 0x21, 0x3f, 0xd5, 0x44,
|
||||
0x48, 0xb4, 0x0f, 0x7e, 0xba, 0xcc, 0x02, 0x6f, 0xe2, 0x4d, 0x07, 0x89, 0x3a, 0x22, 0x04, 0x3b,
|
||||
0x98, 0xe7, 0x22, 0xb8, 0x3d, 0xf1, 0xa7, 0x83, 0x44, 0x9f, 0xd1, 0x73, 0x18, 0x70, 0x22, 0x58,
|
||||
0xcd, 0x53, 0x22, 0x02, 0x7f, 0xe2, 0x4d, 0x87, 0x47, 0x87, 0xf1, 0xbf, 0xf5, 0x64, 0xf3, 0x9b,
|
||||
0x94, 0x71, 0xe2, 0x70, 0xc9, 0x35, 0x05, 0xba, 0x0b, 0x43, 0x21, 0x33, 0x56, 0xcb, 0x79, 0x85,
|
||||
0x65, 0x11, 0xec, 0xe8, 0xec, 0x60, 0x5c, 0x67, 0x58, 0x16, 0x36, 0x80, 0x70, 0x6e, 0x02, 0x3a,
|
||||
0x4d, 0x00, 0xe1, 0x5c, 0x07, 0xec, 0x83, 0x4f, 0xca, 0x55, 0xd0, 0xd5, 0x45, 0xaa, 0xa3, 0xaa,
|
||||
0xbb, 0x16, 0x84, 0x07, 0x3d, 0x1d, 0xab, 0xcf, 0xe8, 0x0e, 0xf4, 0x25, 0x16, 0x97, 0xf3, 0x8c,
|
||||
0xf2, 0xa0, 0xaf, 0xfd, 0x3d, 0x65, 0x9f, 0x50, 0x8e, 0xee, 0xc1, 0x9e, 0xab, 0x67, 0xbe, 0xa0,
|
||||
0x4b, 0x2a, 0x45, 0x30, 0x98, 0x78, 0xd3, 0x7e, 0xb2, 0xeb, 0xdc, 0xa7, 0xda, 0x8b, 0x0e, 0xe1,
|
||||
0x9d, 0x0b, 0x2c, 0x68, 0x3a, 0xaf, 0x38, 0x4b, 0x89, 0x10, 0xf3, 0x34, 0xe7, 0xac, 0xae, 0x02,
|
||||
0xd0, 0xd1, 0x48, 0x7f, 0x3b, 0x33, 0x9f, 0x8e, 0xf5, 0x17, 0x74, 0x02, 0xdd, 0x25, 0xab, 0x4b,
|
||||
0x29, 0x82, 0xe1, 0xc4, 0x9f, 0x0e, 0x8f, 0xee, 0xb7, 0xbc, 0xaa, 0xaf, 0x15, 0x28, 0xb1, 0x58,
|
||||
0xf4, 0x15, 0xf4, 0x32, 0xb2, 0xa2, 0xea, 0xc6, 0x47, 0x9a, 0xe6, 0xe3, 0x96, 0x34, 0x27, 0x1a,
|
||||
0x95, 0x38, 0x74, 0xf4, 0x03, 0xec, 0xba, 0x37, 0x17, 0x15, 0x2b, 0x05, 0x41, 0xcf, 0xa1, 0x67,
|
||||
0x9b, 0xd1, 0x0f, 0x3f, 0x3c, 0x7a, 0x18, 0xb7, 0x1b, 0xd0, 0xd8, 0x36, 0x7a, 0x2e, 0xb1, 0x24,
|
||||
0x89, 0x23, 0x89, 0xc6, 0x30, 0x7c, 0x89, 0xa9, 0xb4, 0x33, 0x15, 0x7d, 0x0f, 0x23, 0x63, 0xfe,
|
||||
0x4f, 0xe9, 0x4e, 0x61, 0xef, 0xbc, 0xa8, 0x65, 0xc6, 0x7e, 0x2e, 0xdd, 0x18, 0x1f, 0x40, 0x57,
|
||||
0xd0, 0xbc, 0xc4, 0x0b, 0x3b, 0xc9, 0xd6, 0x42, 0xef, 0xc3, 0x28, 0xe7, 0x38, 0x25, 0xf3, 0x8a,
|
||||
0x70, 0xca, 0xb2, 0xe0, 0xf6, 0xc4, 0x9b, 0xfa, 0xc9, 0x50, 0xfb, 0xce, 0xb4, 0x2b, 0x42, 0xb0,
|
||||
0x7f, 0xcd, 0x66, 0x2a, 0x8e, 0x0a, 0x38, 0xf8, 0xa6, 0xca, 0x54, 0xd2, 0x66, 0x7a, 0x6d, 0xa2,
|
||||
0x8d, 0x4d, 0xf0, 0xfe, 0xf3, 0x26, 0x44, 0x77, 0xe0, 0xdd, 0x57, 0x32, 0xd9, 0x22, 0xf6, 0x61,
|
||||
0xf7, 0x5b, 0xc2, 0x05, 0x65, 0xae, 0xcb, 0xe8, 0x23, 0xd8, 0x6b, 0x3c, 0xf6, 0x6e, 0x03, 0xe8,
|
||||
0xad, 0x8c, 0xcb, 0x76, 0xee, 0xcc, 0xe8, 0x43, 0x18, 0xa9, 0x7b, 0x6b, 0x2a, 0x0f, 0xa1, 0x4f,
|
||||
0x4b, 0x49, 0xf8, 0xca, 0x5e, 0x92, 0x9f, 0x34, 0x76, 0xf4, 0x12, 0xc6, 0x36, 0xd6, 0xd2, 0x7e,
|
||||
0x09, 0x1d, 0xa1, 0x1c, 0x5b, 0xb6, 0xf8, 0x02, 0x8b, 0x4b, 0x43, 0x64, 0xe0, 0xd1, 0x3d, 0x18,
|
||||
0x9f, 0xeb, 0x97, 0x78, 0xfd, 0x43, 0x75, 0xdc, 0x43, 0xa9, 0x66, 0x5d, 0xa0, 0x6d, 0xff, 0x12,
|
||||
0x86, 0xcf, 0xae, 0x48, 0xea, 0x80, 0x8f, 0xa1, 0x9f, 0x11, 0x9c, 0x2d, 0x68, 0x49, 0x6c, 0x51,
|
||||
0x61, 0x6c, 0xd4, 0x32, 0x76, 0x6a, 0x19, 0xbf, 0x70, 0x6a, 0x99, 0x34, 0xb1, 0x4e, 0xe0, 0x6e,
|
||||
0xbf, 0x2a, 0x70, 0xfe, 0xb5, 0xc0, 0x45, 0xc7, 0x30, 0x32, 0xc9, 0x6c, 0xff, 0x07, 0xd0, 0x65,
|
||||
0xb5, 0xac, 0x6a, 0xa9, 0x73, 0x8d, 0x12, 0x6b, 0xa1, 0xf7, 0x60, 0x40, 0xae, 0xa8, 0x9c, 0xa7,
|
||||
0x2c, 0x23, 0x9a, 0xb3, 0x93, 0xf4, 0x95, 0xe3, 0x98, 0x65, 0x24, 0xfa, 0xdd, 0x83, 0xd1, 0xfa,
|
||||
0xc4, 0xaa, 0xdc, 0x15, 0xcd, 0x6c, 0xa7, 0xea, 0xf8, 0x46, 0xfc, 0xda, 0xdd, 0xf8, 0xeb, 0x77,
|
||||
0x83, 0x62, 0xd8, 0x51, 0xff, 0x03, 0x5a, 0x26, 0xdf, 0xdc, 0xb6, 0x8e, 0x3b, 0xfa, 0x73, 0x00,
|
||||
0xfd, 0x67, 0x76, 0x91, 0xd0, 0x2f, 0xd0, 0x35, 0xdb, 0x8f, 0x1e, 0xb5, 0xdd, 0xba, 0x8d, 0x7f,
|
||||
0x88, 0xf0, 0xf1, 0xb6, 0x30, 0xfb, 0x7e, 0xb7, 0x90, 0x80, 0x1d, 0xa5, 0x03, 0xe8, 0x41, 0x5b,
|
||||
0x86, 0x35, 0x11, 0x09, 0x1f, 0x6e, 0x07, 0x6a, 0x92, 0xfe, 0x06, 0x7d, 0xb7, 0xce, 0xe8, 0x49,
|
||||
0x5b, 0x8e, 0x1b, 0x72, 0x12, 0x7e, 0xb2, 0x3d, 0xb0, 0x29, 0xe0, 0x0f, 0x0f, 0xf6, 0x6e, 0xac,
|
||||
0x34, 0xfa, 0xac, 0x2d, 0xdf, 0xeb, 0x55, 0x27, 0x7c, 0xfa, 0xd6, 0xf8, 0xa6, 0xac, 0x5f, 0xa1,
|
||||
0x67, 0xb5, 0x03, 0xb5, 0x7e, 0xd1, 0x4d, 0xf9, 0x09, 0x9f, 0x6c, 0x8d, 0x6b, 0xb2, 0x5f, 0x41,
|
||||
0x47, 0xeb, 0x02, 0x6a, 0xfd, 0xac, 0xeb, 0xda, 0x15, 0x3e, 0xda, 0x12, 0xe5, 0xf2, 0x1e, 0x7a,
|
||||
0x6a, 0xfe, 0x8d, 0xb0, 0xb4, 0x9f, 0xff, 0x0d, 0xc5, 0x6a, 0x3f, 0xff, 0x37, 0xf4, 0x4b, 0xcf,
|
||||
0xbf, 0x5a, 0xc3, 0xf6, 0xf3, 0xbf, 0xa6, 0x77, 0xed, 0xe7, 0x7f, 0x5d, 0xb7, 0xa2, 0x5b, 0xe8,
|
||||
0x2f, 0x0f, 0xc6, 0xca, 0x75, 0x2e, 0x39, 0xc1, 0x4b, 0x5a, 0xe6, 0xe8, 0x69, 0x4b, 0xf1, 0x56,
|
||||
0x28, 0x23, 0xe0, 0x16, 0xe9, 0x4a, 0xf9, 0xfc, 0xed, 0x09, 0x5c, 0x59, 0x53, 0xef, 0xd0, 0xfb,
|
||||
0xa2, 0xf7, 0x5d, 0xc7, 0x68, 0x56, 0x57, 0xff, 0x3c, 0xf8, 0x27, 0x00, 0x00, 0xff, 0xff, 0xe4,
|
||||
0x09, 0xe7, 0x2c, 0x45, 0x0b, 0x00, 0x00,
|
||||
var fileDescriptor_executor_43dc81e71868eb7b = []byte{
|
||||
// 955 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0x5b, 0x6f, 0x1b, 0x45,
|
||||
0x14, 0xee, 0xc6, 0xf1, 0xed, 0xd8, 0x4e, 0xcc, 0x08, 0x85, 0xad, 0x79, 0xa8, 0xd9, 0x07, 0x6a,
|
||||
0x41, 0x59, 0x47, 0xe9, 0x0d, 0x09, 0x41, 0x11, 0x49, 0x41, 0x48, 0x21, 0x8a, 0xd6, 0x85, 0x4a,
|
||||
0x3c, 0x60, 0x26, 0xbb, 0xc3, 0xee, 0x28, 0xf6, 0xce, 0x32, 0x33, 0xeb, 0x06, 0x09, 0x89, 0x27,
|
||||
0xfe, 0x01, 0x48, 0xfc, 0x38, 0x7e, 0x0c, 0x9a, 0xdb, 0xc6, 0x4e, 0x4b, 0xb5, 0x2e, 0xe2, 0xc9,
|
||||
0x33, 0x67, 0xcf, 0xf7, 0x9d, 0xcb, 0x9c, 0xf3, 0x19, 0xee, 0x25, 0x9c, 0xae, 0x08, 0x17, 0x53,
|
||||
0x91, 0x61, 0x4e, 0x92, 0x29, 0xb9, 0x22, 0x71, 0x29, 0x19, 0x9f, 0x16, 0x9c, 0x49, 0x56, 0x5d,
|
||||
0x43, 0x7d, 0x45, 0xef, 0x67, 0x58, 0x64, 0x34, 0x66, 0xbc, 0x08, 0x73, 0xb6, 0xc4, 0x49, 0x58,
|
||||
0x2c, 0xca, 0x94, 0xe6, 0x22, 0xdc, 0xf4, 0x1b, 0xdd, 0x49, 0x19, 0x4b, 0x17, 0xc4, 0x90, 0x5c,
|
||||
0x94, 0x3f, 0x4d, 0x25, 0x5d, 0x12, 0x21, 0xf1, 0xb2, 0xb0, 0x0e, 0x9f, 0xa6, 0x54, 0x66, 0xe5,
|
||||
0x45, 0x18, 0xb3, 0xe5, 0xb4, 0xe2, 0x9c, 0x6a, 0xce, 0xa9, 0xe5, 0x9c, 0xba, 0xcc, 0x4c, 0x26,
|
||||
0xe6, 0x66, 0xe0, 0xc1, 0xdf, 0xbb, 0x30, 0x38, 0xc5, 0x65, 0x1e, 0x67, 0x11, 0xf9, 0xb9, 0x24,
|
||||
0x42, 0xa2, 0x21, 0x34, 0xe2, 0x65, 0xe2, 0x7b, 0x63, 0x6f, 0xd2, 0x8d, 0xd4, 0x11, 0x21, 0xd8,
|
||||
0xc5, 0x3c, 0x15, 0xfe, 0xce, 0xb8, 0x31, 0xe9, 0x46, 0xfa, 0x8c, 0xce, 0xa0, 0xcb, 0x89, 0x60,
|
||||
0x25, 0x8f, 0x89, 0xf0, 0x1b, 0x63, 0x6f, 0xd2, 0x3b, 0x3a, 0x0c, 0xff, 0xad, 0x26, 0x1b, 0xdf,
|
||||
0x84, 0x0c, 0x23, 0x87, 0x8b, 0xae, 0x29, 0xd0, 0x1d, 0xe8, 0x09, 0x99, 0xb0, 0x52, 0xce, 0x0b,
|
||||
0x2c, 0x33, 0x7f, 0x57, 0x47, 0x07, 0x63, 0x3a, 0xc7, 0x32, 0xb3, 0x0e, 0x84, 0x73, 0xe3, 0xd0,
|
||||
0xac, 0x1c, 0x08, 0xe7, 0xda, 0x61, 0x08, 0x0d, 0x92, 0xaf, 0xfc, 0x96, 0x4e, 0x52, 0x1d, 0x55,
|
||||
0xde, 0xa5, 0x20, 0xdc, 0x6f, 0x6b, 0x5f, 0x7d, 0x46, 0xb7, 0xa1, 0x23, 0xb1, 0xb8, 0x9c, 0x27,
|
||||
0x94, 0xfb, 0x1d, 0x6d, 0x6f, 0xab, 0xfb, 0x09, 0xe5, 0xe8, 0x2e, 0xec, 0xbb, 0x7c, 0xe6, 0x0b,
|
||||
0xba, 0xa4, 0x52, 0xf8, 0xdd, 0xb1, 0x37, 0xe9, 0x44, 0x7b, 0xce, 0x7c, 0xaa, 0xad, 0xe8, 0x10,
|
||||
0xde, 0xbe, 0xc0, 0x82, 0xc6, 0xf3, 0x82, 0xb3, 0x98, 0x08, 0x31, 0x8f, 0x53, 0xce, 0xca, 0xc2,
|
||||
0x07, 0xed, 0x8d, 0xf4, 0xb7, 0x73, 0xf3, 0xe9, 0x58, 0x7f, 0x41, 0x27, 0xd0, 0x5a, 0xb2, 0x32,
|
||||
0x97, 0xc2, 0xef, 0x8d, 0x1b, 0x93, 0xde, 0xd1, 0xbd, 0x9a, 0xad, 0xfa, 0x46, 0x81, 0x22, 0x8b,
|
||||
0x45, 0x5f, 0x41, 0x3b, 0x21, 0x2b, 0xaa, 0x3a, 0xde, 0xd7, 0x34, 0x1f, 0xd5, 0xa4, 0x39, 0xd1,
|
||||
0xa8, 0xc8, 0xa1, 0x51, 0x06, 0x6f, 0xe5, 0x44, 0xbe, 0x60, 0xfc, 0x72, 0x4e, 0x05, 0x5b, 0x60,
|
||||
0x49, 0x59, 0xee, 0x0f, 0xf4, 0x23, 0x7e, 0x52, 0x93, 0xf2, 0xcc, 0xe0, 0xbf, 0x76, 0xf0, 0x59,
|
||||
0x41, 0xe2, 0x68, 0x98, 0xdf, 0xb0, 0x06, 0x3f, 0xc2, 0x9e, 0x9b, 0x2e, 0x51, 0xb0, 0x5c, 0x10,
|
||||
0x74, 0x06, 0x6d, 0xdb, 0x36, 0x3d, 0x62, 0xbd, 0xa3, 0x07, 0x61, 0xbd, 0x55, 0x08, 0x6d, 0x4b,
|
||||
0x67, 0x12, 0x4b, 0x12, 0x39, 0x92, 0x60, 0x00, 0xbd, 0xe7, 0x98, 0x4a, 0x3b, 0xbd, 0xc1, 0x0f,
|
||||
0xd0, 0x37, 0xd7, 0xff, 0x29, 0xdc, 0x29, 0xec, 0xcf, 0xb2, 0x52, 0x26, 0xec, 0x45, 0xee, 0x16,
|
||||
0xe6, 0x00, 0x5a, 0x82, 0xa6, 0x39, 0x5e, 0xd8, 0x9d, 0xb1, 0x37, 0xf4, 0x1e, 0xf4, 0x53, 0x8e,
|
||||
0x63, 0x32, 0x2f, 0x08, 0xa7, 0x2c, 0xf1, 0x77, 0xc6, 0xde, 0xa4, 0x11, 0xf5, 0xb4, 0xed, 0x5c,
|
||||
0x9b, 0x02, 0x04, 0xc3, 0x6b, 0x36, 0x93, 0x71, 0x90, 0xc1, 0xc1, 0xb7, 0x45, 0xa2, 0x82, 0x56,
|
||||
0x7b, 0x62, 0x03, 0x6d, 0xec, 0x9c, 0xf7, 0x9f, 0x77, 0x2e, 0xb8, 0x0d, 0xef, 0xbc, 0x14, 0xc9,
|
||||
0x26, 0x31, 0x84, 0xbd, 0xef, 0x08, 0x17, 0x94, 0xb9, 0x2a, 0x83, 0x0f, 0x61, 0xbf, 0xb2, 0xd8,
|
||||
0xde, 0xfa, 0xd0, 0x5e, 0x19, 0x93, 0xad, 0xdc, 0x5d, 0x83, 0x0f, 0xa0, 0xaf, 0xfa, 0x56, 0x65,
|
||||
0x3e, 0x82, 0x0e, 0xcd, 0x25, 0xe1, 0x2b, 0xdb, 0xa4, 0x46, 0x54, 0xdd, 0x83, 0xe7, 0x30, 0xb0,
|
||||
0xbe, 0x96, 0xf6, 0x4b, 0x68, 0x0a, 0x65, 0xd8, 0xb2, 0xc4, 0x67, 0x58, 0x5c, 0x1a, 0x22, 0x03,
|
||||
0x0f, 0xee, 0xc2, 0x60, 0xa6, 0x5f, 0xe2, 0xd5, 0x0f, 0xd5, 0x74, 0x0f, 0xa5, 0x8a, 0x75, 0x8e,
|
||||
0xb6, 0xfc, 0x4b, 0xe8, 0x3d, 0xbd, 0x22, 0xb1, 0x03, 0x3e, 0x82, 0x4e, 0x42, 0x70, 0xb2, 0xa0,
|
||||
0x39, 0xb1, 0x49, 0x8d, 0x42, 0xa3, 0xcb, 0xa1, 0xd3, 0xe5, 0xf0, 0x99, 0xd3, 0xe5, 0xa8, 0xf2,
|
||||
0x75, 0x52, 0xba, 0xf3, 0xb2, 0x94, 0x36, 0xae, 0xa5, 0x34, 0x38, 0x86, 0xbe, 0x09, 0x66, 0xeb,
|
||||
0x3f, 0x80, 0x16, 0x2b, 0x65, 0x51, 0x4a, 0x1d, 0xab, 0x1f, 0xd9, 0x1b, 0x7a, 0x17, 0xba, 0xe4,
|
||||
0x8a, 0xca, 0x79, 0xcc, 0x12, 0xa2, 0x39, 0x9b, 0x51, 0x47, 0x19, 0x8e, 0x59, 0x42, 0x82, 0xdf,
|
||||
0x3d, 0xe8, 0xaf, 0x4f, 0xac, 0x8a, 0x5d, 0xd0, 0xc4, 0x56, 0xaa, 0x8e, 0xaf, 0xc5, 0xaf, 0xf5,
|
||||
0xa6, 0xb1, 0xde, 0x1b, 0x14, 0xc2, 0xae, 0xfa, 0xc7, 0xd1, 0x82, 0xfc, 0xfa, 0xb2, 0xb5, 0xdf,
|
||||
0xd1, 0x9f, 0x5d, 0xe8, 0x3c, 0xb5, 0x8b, 0x84, 0x7e, 0x81, 0x96, 0xd9, 0x7e, 0xf4, 0xb0, 0xee,
|
||||
0xd6, 0x6d, 0xfc, 0x17, 0x8d, 0x1e, 0x6d, 0x0b, 0xb3, 0xef, 0x77, 0x0b, 0x09, 0xd8, 0x55, 0x3a,
|
||||
0x80, 0xee, 0xd7, 0x65, 0x58, 0x13, 0x91, 0xd1, 0x83, 0xed, 0x40, 0x55, 0xd0, 0xdf, 0xa0, 0xe3,
|
||||
0xd6, 0x19, 0x3d, 0xae, 0xcb, 0x71, 0x43, 0x4e, 0x46, 0x1f, 0x6f, 0x0f, 0xac, 0x12, 0xf8, 0xc3,
|
||||
0x83, 0xfd, 0x1b, 0x2b, 0x8d, 0x3e, 0xab, 0xcb, 0xf7, 0x6a, 0xd5, 0x19, 0x3d, 0x79, 0x63, 0x7c,
|
||||
0x95, 0xd6, 0xaf, 0xd0, 0xb6, 0xda, 0x81, 0x6a, 0xbf, 0xe8, 0xa6, 0xfc, 0x8c, 0x1e, 0x6f, 0x8d,
|
||||
0xab, 0xa2, 0x5f, 0x41, 0x53, 0xeb, 0x02, 0xaa, 0xfd, 0xac, 0xeb, 0xda, 0x35, 0x7a, 0xb8, 0x25,
|
||||
0xca, 0xc5, 0x3d, 0xf4, 0xd4, 0xfc, 0x1b, 0x61, 0xa9, 0x3f, 0xff, 0x1b, 0x8a, 0x55, 0x7f, 0xfe,
|
||||
0x6f, 0xe8, 0x97, 0x9e, 0x7f, 0xb5, 0x86, 0xf5, 0xe7, 0x7f, 0x4d, 0xef, 0xea, 0xcf, 0xff, 0xba,
|
||||
0x6e, 0x05, 0xb7, 0xd0, 0x5f, 0x1e, 0x0c, 0x94, 0x69, 0x26, 0x39, 0xc1, 0x4b, 0x9a, 0xa7, 0xe8,
|
||||
0x49, 0x4d, 0xf1, 0x56, 0x28, 0x23, 0xe0, 0x16, 0xe9, 0x52, 0xf9, 0xfc, 0xcd, 0x09, 0x5c, 0x5a,
|
||||
0x13, 0xef, 0xd0, 0xfb, 0xa2, 0xfd, 0x7d, 0xd3, 0x68, 0x56, 0x4b, 0xff, 0xdc, 0xff, 0x27, 0x00,
|
||||
0x00, 0xff, 0xff, 0xad, 0xfe, 0x69, 0xb2, 0xaf, 0x0b, 0x00, 0x00,
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ message LaunchRequest {
|
|||
bool basic_process_cgroup = 10;
|
||||
repeated hashicorp.nomad.plugins.drivers.proto.Mount mounts = 11;
|
||||
repeated hashicorp.nomad.plugins.drivers.proto.Device devices = 12;
|
||||
hashicorp.nomad.plugins.drivers.proto.NetworkIsolationSpec network_isolation = 13;
|
||||
}
|
||||
|
||||
message LaunchResponse {
|
||||
|
|
|
@ -33,6 +33,7 @@ func (s *grpcExecutorServer) Launch(ctx context.Context, req *proto.LaunchReques
|
|||
BasicProcessCgroup: req.BasicProcessCgroup,
|
||||
Mounts: drivers.MountsFromProto(req.Mounts),
|
||||
Devices: drivers.DevicesFromProto(req.Devices),
|
||||
NetworkIsolation: drivers.NetworkIsolationSpecFromProto(req.NetworkIsolation),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -190,6 +190,31 @@ func SliceSetDisjoint(first, second []string) (bool, []string) {
|
|||
return false, flattened
|
||||
}
|
||||
|
||||
// CompareSliceSetString returns true if the slices contain the same strings.
|
||||
// Order is ignored. The slice may be copied but is never altered. The slice is
|
||||
// assumed to be a set. Multiple instances of an entry are treated the same as
|
||||
// a single instance.
|
||||
func CompareSliceSetString(a, b []string) bool {
|
||||
n := len(a)
|
||||
if n != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Copy a into a map and compare b against it
|
||||
amap := make(map[string]struct{}, n)
|
||||
for i := range a {
|
||||
amap[a[i]] = struct{}{}
|
||||
}
|
||||
|
||||
for i := range b {
|
||||
if _, ok := amap[b[i]]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// CompareMapStringString returns true if the maps are equivalent. A nil and
|
||||
// empty map are considered not equal.
|
||||
func CompareMapStringString(a, b map[string]string) bool {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
@ -21,6 +22,75 @@ func TestSliceStringIsSubset(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCompareSliceSetString(t *testing.T) {
|
||||
cases := []struct {
|
||||
A []string
|
||||
B []string
|
||||
Result bool
|
||||
}{
|
||||
{
|
||||
A: []string{},
|
||||
B: []string{},
|
||||
Result: true,
|
||||
},
|
||||
{
|
||||
A: []string{},
|
||||
B: []string{"a"},
|
||||
Result: false,
|
||||
},
|
||||
{
|
||||
A: []string{"a"},
|
||||
B: []string{"a"},
|
||||
Result: true,
|
||||
},
|
||||
{
|
||||
A: []string{"a"},
|
||||
B: []string{"b"},
|
||||
Result: false,
|
||||
},
|
||||
{
|
||||
A: []string{"a", "b"},
|
||||
B: []string{"b"},
|
||||
Result: false,
|
||||
},
|
||||
{
|
||||
A: []string{"a", "b"},
|
||||
B: []string{"a"},
|
||||
Result: false,
|
||||
},
|
||||
{
|
||||
A: []string{"a", "b"},
|
||||
B: []string{"a", "b"},
|
||||
Result: true,
|
||||
},
|
||||
{
|
||||
A: []string{"a", "b"},
|
||||
B: []string{"b", "a"},
|
||||
Result: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(fmt.Sprintf("case-%da", i), func(t *testing.T) {
|
||||
if res := CompareSliceSetString(tc.A, tc.B); res != tc.Result {
|
||||
t.Fatalf("expected %t but CompareSliceSetString(%v, %v) -> %t",
|
||||
tc.Result, tc.A, tc.B, res,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Function is commutative so compare B and A
|
||||
t.Run(fmt.Sprintf("case-%db", i), func(t *testing.T) {
|
||||
if res := CompareSliceSetString(tc.B, tc.A); res != tc.Result {
|
||||
t.Fatalf("expected %t but CompareSliceSetString(%v, %v) -> %t",
|
||||
tc.Result, tc.B, tc.A, res,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapStringStringSliceValueSet(t *testing.T) {
|
||||
m := map[string][]string{
|
||||
"foo": {"1", "2"},
|
||||
|
|
326
jobspec/parse.go
326
jobspec/parse.go
|
@ -314,6 +314,8 @@ func parseGroups(result *api.Job, list *ast.ObjectList) error {
|
|||
"vault",
|
||||
"migrate",
|
||||
"spread",
|
||||
"network",
|
||||
"service",
|
||||
}
|
||||
if err := helper.CheckHCLKeys(listVal, valid); err != nil {
|
||||
return multierror.Prefix(err, fmt.Sprintf("'%s' ->", n))
|
||||
|
@ -333,6 +335,8 @@ func parseGroups(result *api.Job, list *ast.ObjectList) error {
|
|||
delete(m, "vault")
|
||||
delete(m, "migrate")
|
||||
delete(m, "spread")
|
||||
delete(m, "network")
|
||||
delete(m, "service")
|
||||
|
||||
// Build the group with the basic decode
|
||||
var g api.TaskGroup
|
||||
|
@ -369,6 +373,15 @@ func parseGroups(result *api.Job, list *ast.ObjectList) error {
|
|||
}
|
||||
}
|
||||
|
||||
// Parse network
|
||||
if o := listVal.Filter("network"); len(o.Items) > 0 {
|
||||
networks, err := parseNetwork(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
g.Networks = []*api.NetworkResource{networks}
|
||||
}
|
||||
|
||||
// Parse reschedule policy
|
||||
if o := listVal.Filter("reschedule"); len(o.Items) > 0 {
|
||||
if err := parseReschedulePolicy(&g.ReschedulePolicy, o); err != nil {
|
||||
|
@ -437,6 +450,12 @@ func parseGroups(result *api.Job, list *ast.ObjectList) error {
|
|||
}
|
||||
}
|
||||
|
||||
if o := listVal.Filter("service"); len(o.Items) > 0 {
|
||||
if err := parseGroupServices(*result.Name, *g.Name, &g, o); err != nil {
|
||||
return multierror.Prefix(err, fmt.Sprintf("'%s',", n))
|
||||
}
|
||||
}
|
||||
|
||||
collection = append(collection, &g)
|
||||
}
|
||||
|
||||
|
@ -1191,6 +1210,83 @@ func parseTemplates(result *[]*api.Template, list *ast.ObjectList) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
//TODO(schmichael) combine with non-group services
|
||||
func parseGroupServices(jobName string, taskGroupName string, g *api.TaskGroup, serviceObjs *ast.ObjectList) error {
|
||||
g.Services = make([]*api.Service, len(serviceObjs.Items))
|
||||
for idx, o := range serviceObjs.Items {
|
||||
// Check for invalid keys
|
||||
valid := []string{
|
||||
"name",
|
||||
"tags",
|
||||
"canary_tags",
|
||||
"port",
|
||||
"check",
|
||||
"address_mode",
|
||||
"check_restart",
|
||||
"connect",
|
||||
}
|
||||
if err := helper.CheckHCLKeys(o.Val, valid); err != nil {
|
||||
return multierror.Prefix(err, fmt.Sprintf("service (%d) ->", idx))
|
||||
}
|
||||
|
||||
var service api.Service
|
||||
var m map[string]interface{}
|
||||
if err := hcl.DecodeObject(&m, o.Val); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
delete(m, "check")
|
||||
delete(m, "check_restart")
|
||||
delete(m, "connect")
|
||||
|
||||
if err := mapstructure.WeakDecode(m, &service); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Filter checks
|
||||
var checkList *ast.ObjectList
|
||||
if ot, ok := o.Val.(*ast.ObjectType); ok {
|
||||
checkList = ot.List
|
||||
} else {
|
||||
return fmt.Errorf("service '%s': should be an object", service.Name)
|
||||
}
|
||||
|
||||
if co := checkList.Filter("check"); len(co.Items) > 0 {
|
||||
if err := parseChecks(&service, co); err != nil {
|
||||
return multierror.Prefix(err, fmt.Sprintf("service: '%s',", service.Name))
|
||||
}
|
||||
}
|
||||
|
||||
// Filter check_restart
|
||||
if cro := checkList.Filter("check_restart"); len(cro.Items) > 0 {
|
||||
if len(cro.Items) > 1 {
|
||||
return fmt.Errorf("check_restart '%s': cannot have more than 1 check_restart", service.Name)
|
||||
}
|
||||
if cr, err := parseCheckRestart(cro.Items[0]); err != nil {
|
||||
return multierror.Prefix(err, fmt.Sprintf("service: '%s',", service.Name))
|
||||
} else {
|
||||
service.CheckRestart = cr
|
||||
}
|
||||
}
|
||||
|
||||
// Filter connect
|
||||
if co := checkList.Filter("connect"); len(co.Items) > 0 {
|
||||
if len(co.Items) > 1 {
|
||||
return fmt.Errorf("connect '%s': cannot have more than 1 connect", service.Name)
|
||||
}
|
||||
if c, err := parseConnect(co.Items[0]); err != nil {
|
||||
return multierror.Prefix(err, fmt.Sprintf("service: '%s',", service.Name))
|
||||
} else {
|
||||
service.Connect = c
|
||||
}
|
||||
}
|
||||
|
||||
g.Services[idx] = &service
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseServices(jobName string, taskGroupName string, task *api.Task, serviceObjs *ast.ObjectList) error {
|
||||
task.Services = make([]*api.Service, len(serviceObjs.Items))
|
||||
for idx, o := range serviceObjs.Items {
|
||||
|
@ -1387,6 +1483,162 @@ func parseCheckRestart(cro *ast.ObjectItem) (*api.CheckRestart, error) {
|
|||
return &checkRestart, nil
|
||||
}
|
||||
|
||||
func parseConnect(co *ast.ObjectItem) (*api.ConsulConnect, error) {
|
||||
valid := []string{
|
||||
"sidecar_service",
|
||||
}
|
||||
|
||||
if err := helper.CheckHCLKeys(co.Val, valid); err != nil {
|
||||
return nil, multierror.Prefix(err, "connect ->")
|
||||
}
|
||||
|
||||
var connect api.ConsulConnect
|
||||
|
||||
var connectList *ast.ObjectList
|
||||
if ot, ok := co.Val.(*ast.ObjectType); ok {
|
||||
connectList = ot.List
|
||||
} else {
|
||||
return nil, fmt.Errorf("connect should be an object")
|
||||
}
|
||||
|
||||
// Parse the sidecar_service
|
||||
o := connectList.Filter("sidecar_service")
|
||||
if len(o.Items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if len(o.Items) > 1 {
|
||||
return nil, fmt.Errorf("only one 'sidecar_service' block allowed per task")
|
||||
}
|
||||
|
||||
r, err := parseSidecarService(o.Items[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sidecar_service, %v", err)
|
||||
}
|
||||
connect.SidecarService = r
|
||||
|
||||
return &connect, nil
|
||||
}
|
||||
|
||||
func parseSidecarService(o *ast.ObjectItem) (*api.ConsulSidecarService, error) {
|
||||
valid := []string{
|
||||
"port",
|
||||
"proxy",
|
||||
}
|
||||
|
||||
if err := helper.CheckHCLKeys(o.Val, valid); err != nil {
|
||||
return nil, multierror.Prefix(err, "sidecar_service ->")
|
||||
}
|
||||
|
||||
var sidecar api.ConsulSidecarService
|
||||
var m map[string]interface{}
|
||||
if err := hcl.DecodeObject(&m, o.Val); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
delete(m, "proxy")
|
||||
|
||||
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
|
||||
WeaklyTypedInput: true,
|
||||
Result: &sidecar,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := dec.Decode(m); err != nil {
|
||||
return nil, fmt.Errorf("foo: %v", err)
|
||||
}
|
||||
|
||||
var proxyList *ast.ObjectList
|
||||
if ot, ok := o.Val.(*ast.ObjectType); ok {
|
||||
proxyList = ot.List
|
||||
} else {
|
||||
return nil, fmt.Errorf("sidecar_service: should be an object")
|
||||
}
|
||||
|
||||
// Parse the proxy
|
||||
po := proxyList.Filter("proxy")
|
||||
if len(po.Items) == 0 {
|
||||
return &sidecar, nil
|
||||
}
|
||||
if len(po.Items) > 1 {
|
||||
return nil, fmt.Errorf("only one 'proxy' block allowed per task")
|
||||
}
|
||||
|
||||
r, err := parseProxy(po.Items[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("proxy, %v", err)
|
||||
}
|
||||
sidecar.Proxy = r
|
||||
|
||||
return &sidecar, nil
|
||||
}
|
||||
|
||||
func parseProxy(o *ast.ObjectItem) (*api.ConsulProxy, error) {
|
||||
valid := []string{
|
||||
"upstreams",
|
||||
}
|
||||
|
||||
if err := helper.CheckHCLKeys(o.Val, valid); err != nil {
|
||||
return nil, multierror.Prefix(err, "proxy ->")
|
||||
}
|
||||
|
||||
var proxy api.ConsulProxy
|
||||
|
||||
var listVal *ast.ObjectList
|
||||
if ot, ok := o.Val.(*ast.ObjectType); ok {
|
||||
listVal = ot.List
|
||||
} else {
|
||||
return nil, fmt.Errorf("proxy: should be an object")
|
||||
}
|
||||
|
||||
// Parse the proxy
|
||||
uo := listVal.Filter("upstreams")
|
||||
proxy.Upstreams = make([]*api.ConsulUpstream, len(uo.Items))
|
||||
for i := range uo.Items {
|
||||
u, err := parseUpstream(uo.Items[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy.Upstreams[i] = u
|
||||
}
|
||||
|
||||
return &proxy, nil
|
||||
}
|
||||
|
||||
func parseUpstream(uo *ast.ObjectItem) (*api.ConsulUpstream, error) {
|
||||
valid := []string{
|
||||
"destination_name",
|
||||
"local_bind_port",
|
||||
}
|
||||
|
||||
if err := helper.CheckHCLKeys(uo.Val, valid); err != nil {
|
||||
return nil, multierror.Prefix(err, "upstream ->")
|
||||
}
|
||||
|
||||
var upstream api.ConsulUpstream
|
||||
var m map[string]interface{}
|
||||
if err := hcl.DecodeObject(&m, uo.Val); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
|
||||
WeaklyTypedInput: true,
|
||||
Result: &upstream,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := dec.Decode(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &upstream, nil
|
||||
}
|
||||
|
||||
func parseResources(result *api.Resources, list *ast.ObjectList) error {
|
||||
list = list.Elem()
|
||||
if len(list.Items) == 0 {
|
||||
|
@ -1433,39 +1685,11 @@ func parseResources(result *api.Resources, list *ast.ObjectList) error {
|
|||
|
||||
// Parse the network resources
|
||||
if o := listVal.Filter("network"); len(o.Items) > 0 {
|
||||
if len(o.Items) > 1 {
|
||||
return fmt.Errorf("only one 'network' resource allowed")
|
||||
r, err := parseNetwork(o)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resource, %v", err)
|
||||
}
|
||||
|
||||
// Check for invalid keys
|
||||
valid := []string{
|
||||
"mbits",
|
||||
"port",
|
||||
}
|
||||
if err := helper.CheckHCLKeys(o.Items[0].Val, valid); err != nil {
|
||||
return multierror.Prefix(err, "resources, network ->")
|
||||
}
|
||||
|
||||
var r api.NetworkResource
|
||||
var m map[string]interface{}
|
||||
if err := hcl.DecodeObject(&m, o.Items[0].Val); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := mapstructure.WeakDecode(m, &r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var networkObj *ast.ObjectList
|
||||
if ot, ok := o.Items[0].Val.(*ast.ObjectType); ok {
|
||||
networkObj = ot.List
|
||||
} else {
|
||||
return fmt.Errorf("resource: should be an object")
|
||||
}
|
||||
if err := parsePorts(networkObj, &r); err != nil {
|
||||
return multierror.Prefix(err, "resources, network, ports ->")
|
||||
}
|
||||
|
||||
result.Networks = []*api.NetworkResource{&r}
|
||||
result.Networks = []*api.NetworkResource{r}
|
||||
}
|
||||
|
||||
// Parse the device resources
|
||||
|
@ -1535,11 +1759,49 @@ func parseResources(result *api.Resources, list *ast.ObjectList) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func parseNetwork(o *ast.ObjectList) (*api.NetworkResource, error) {
|
||||
if len(o.Items) > 1 {
|
||||
return nil, fmt.Errorf("only one 'network' resource allowed")
|
||||
}
|
||||
|
||||
// Check for invalid keys
|
||||
valid := []string{
|
||||
"mode",
|
||||
"mbits",
|
||||
"port",
|
||||
}
|
||||
if err := helper.CheckHCLKeys(o.Items[0].Val, valid); err != nil {
|
||||
return nil, multierror.Prefix(err, "network ->")
|
||||
}
|
||||
|
||||
var r api.NetworkResource
|
||||
var m map[string]interface{}
|
||||
if err := hcl.DecodeObject(&m, o.Items[0].Val); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := mapstructure.WeakDecode(m, &r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var networkObj *ast.ObjectList
|
||||
if ot, ok := o.Items[0].Val.(*ast.ObjectType); ok {
|
||||
networkObj = ot.List
|
||||
} else {
|
||||
return nil, fmt.Errorf("should be an object")
|
||||
}
|
||||
if err := parsePorts(networkObj, &r); err != nil {
|
||||
return nil, multierror.Prefix(err, "network, ports ->")
|
||||
}
|
||||
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
func parsePorts(networkObj *ast.ObjectList, nw *api.NetworkResource) error {
|
||||
// Check for invalid keys
|
||||
valid := []string{
|
||||
"mbits",
|
||||
"port",
|
||||
"mode",
|
||||
}
|
||||
if err := helper.CheckHCLKeys(networkObj, valid); err != nil {
|
||||
return err
|
||||
|
|
|
@ -871,6 +871,50 @@ func TestParse(t *testing.T) {
|
|||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"tg-network.hcl",
|
||||
&api.Job{
|
||||
ID: helper.StringToPtr("foo"),
|
||||
Name: helper.StringToPtr("foo"),
|
||||
Datacenters: []string{"dc1"},
|
||||
TaskGroups: []*api.TaskGroup{
|
||||
{
|
||||
Name: helper.StringToPtr("bar"),
|
||||
Count: helper.IntToPtr(3),
|
||||
Networks: []*api.NetworkResource{
|
||||
{
|
||||
Mode: "bridge",
|
||||
ReservedPorts: []api.Port{
|
||||
{
|
||||
Label: "http",
|
||||
Value: 80,
|
||||
To: 8080,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Tasks: []*api.Task{
|
||||
{
|
||||
Name: "bar",
|
||||
Driver: "raw_exec",
|
||||
Config: map[string]interface{}{
|
||||
"command": "bash",
|
||||
"args": []interface{}{"-c", "echo hi"},
|
||||
},
|
||||
Resources: &api.Resources{
|
||||
Networks: []*api.NetworkResource{
|
||||
{
|
||||
MBits: helper.IntToPtr(10),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
job "foo" {
|
||||
datacenters = ["dc1"]
|
||||
group "bar" {
|
||||
count = 3
|
||||
network {
|
||||
mode = "bridge"
|
||||
port "http" {
|
||||
static = 80
|
||||
to = 8080
|
||||
}
|
||||
}
|
||||
task "bar" {
|
||||
driver = "raw_exec"
|
||||
config {
|
||||
command = "bash"
|
||||
args = ["-c", "echo hi"]
|
||||
}
|
||||
resources {
|
||||
network {
|
||||
mbits = 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
package structs
|
||||
|
||||
type ConsulConnect struct {
|
||||
SidecarService *ConsulSidecarService
|
||||
}
|
||||
|
||||
func (c *ConsulConnect) Copy() *ConsulConnect {
|
||||
return &ConsulConnect{
|
||||
SidecarService: c.SidecarService.Copy(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ConsulConnect) Equals(o *ConsulConnect) bool {
|
||||
if c == nil || o == nil {
|
||||
return c == o
|
||||
}
|
||||
|
||||
return c.SidecarService.Equals(o.SidecarService)
|
||||
}
|
||||
|
||||
func (c *ConsulConnect) HasSidecar() bool {
|
||||
return c != nil && c.SidecarService != nil
|
||||
}
|
||||
|
||||
type ConsulSidecarService struct {
|
||||
Port string
|
||||
Proxy *ConsulProxy
|
||||
}
|
||||
|
||||
func (s *ConsulSidecarService) Copy() *ConsulSidecarService {
|
||||
return &ConsulSidecarService{
|
||||
Port: s.Port,
|
||||
Proxy: s.Proxy.Copy(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ConsulSidecarService) Equals(o *ConsulSidecarService) bool {
|
||||
if s == nil || o == nil {
|
||||
return s == o
|
||||
}
|
||||
|
||||
if s.Port != o.Port {
|
||||
return false
|
||||
}
|
||||
|
||||
return s.Proxy.Equals(o.Proxy)
|
||||
}
|
||||
|
||||
type ConsulProxy struct {
|
||||
Upstreams []*ConsulUpstream
|
||||
}
|
||||
|
||||
func (p *ConsulProxy) Copy() *ConsulProxy {
|
||||
upstreams := make([]*ConsulUpstream, len(p.Upstreams))
|
||||
|
||||
for i := range p.Upstreams {
|
||||
upstreams[i] = p.Upstreams[i].Copy()
|
||||
}
|
||||
|
||||
return &ConsulProxy{
|
||||
Upstreams: upstreams,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ConsulProxy) Equals(o *ConsulProxy) bool {
|
||||
if p == nil || o == nil {
|
||||
return p == o
|
||||
}
|
||||
|
||||
if len(p.Upstreams) != len(o.Upstreams) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Order doesn't matter
|
||||
OUTER:
|
||||
for _, up := range p.Upstreams {
|
||||
for _, innerUp := range o.Upstreams {
|
||||
if up.Equals(innerUp) {
|
||||
// Match; find next upstream
|
||||
continue OUTER
|
||||
}
|
||||
}
|
||||
|
||||
// No match
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type ConsulUpstream struct {
|
||||
DestinationName string
|
||||
LocalBindPort int
|
||||
}
|
||||
|
||||
func (u *ConsulUpstream) Copy() *ConsulUpstream {
|
||||
return &ConsulUpstream{
|
||||
DestinationName: u.DestinationName,
|
||||
LocalBindPort: u.LocalBindPort,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ConsulUpstream) Equals(o *ConsulUpstream) bool {
|
||||
if u == nil || o == nil {
|
||||
return u == o
|
||||
}
|
||||
|
||||
return (*u) == (*o)
|
||||
}
|
|
@ -3317,6 +3317,7 @@ func TestTaskDiff(t *testing.T) {
|
|||
DynamicPorts: []Port{
|
||||
{
|
||||
Label: "bar",
|
||||
To: 8080,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -3340,6 +3341,7 @@ func TestTaskDiff(t *testing.T) {
|
|||
DynamicPorts: []Port{
|
||||
{
|
||||
Label: "baz",
|
||||
To: 8081,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -3375,6 +3377,12 @@ func TestTaskDiff(t *testing.T) {
|
|||
Old: "",
|
||||
New: "foo",
|
||||
},
|
||||
{
|
||||
Type: DiffTypeAdded,
|
||||
Name: "To",
|
||||
Old: "",
|
||||
New: "0",
|
||||
},
|
||||
{
|
||||
Type: DiffTypeAdded,
|
||||
Name: "Value",
|
||||
|
@ -3393,6 +3401,12 @@ func TestTaskDiff(t *testing.T) {
|
|||
Old: "",
|
||||
New: "baz",
|
||||
},
|
||||
{
|
||||
Type: DiffTypeAdded,
|
||||
Name: "To",
|
||||
Old: "",
|
||||
New: "8081",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -3419,6 +3433,12 @@ func TestTaskDiff(t *testing.T) {
|
|||
Old: "foo",
|
||||
New: "",
|
||||
},
|
||||
{
|
||||
Type: DiffTypeDeleted,
|
||||
Name: "To",
|
||||
Old: "0",
|
||||
New: "",
|
||||
},
|
||||
{
|
||||
Type: DiffTypeDeleted,
|
||||
Name: "Value",
|
||||
|
@ -3437,6 +3457,12 @@ func TestTaskDiff(t *testing.T) {
|
|||
Old: "bar",
|
||||
New: "",
|
||||
},
|
||||
{
|
||||
Type: DiffTypeDeleted,
|
||||
Name: "To",
|
||||
Old: "8080",
|
||||
New: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -3879,6 +3905,12 @@ func TestTaskDiff(t *testing.T) {
|
|||
Old: "boom_port",
|
||||
New: "boom_port",
|
||||
},
|
||||
{
|
||||
Type: DiffTypeNone,
|
||||
Name: "boom.To",
|
||||
Old: "0",
|
||||
New: "0",
|
||||
},
|
||||
{
|
||||
Type: DiffTypeNone,
|
||||
Name: "boom.Value",
|
||||
|
|
|
@ -108,7 +108,7 @@ func TestAllocsFit_PortsOvercommitted_Old(t *testing.T) {
|
|||
Device: "eth0",
|
||||
IP: "10.0.0.1",
|
||||
MBits: 50,
|
||||
ReservedPorts: []Port{{"main", 8000}},
|
||||
ReservedPorts: []Port{{"main", 8000, 80}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -160,7 +160,7 @@ func TestAllocsFit_Old(t *testing.T) {
|
|||
Device: "eth0",
|
||||
IP: "10.0.0.1",
|
||||
MBits: 50,
|
||||
ReservedPorts: []Port{{"main", 80}},
|
||||
ReservedPorts: []Port{{"main", 80, 0}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -176,7 +176,7 @@ func TestAllocsFit_Old(t *testing.T) {
|
|||
Device: "eth0",
|
||||
IP: "10.0.0.1",
|
||||
MBits: 50,
|
||||
ReservedPorts: []Port{{"main", 8000}},
|
||||
ReservedPorts: []Port{{"main", 8000, 80}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -227,7 +227,7 @@ func TestAllocsFit_TerminalAlloc_Old(t *testing.T) {
|
|||
Device: "eth0",
|
||||
IP: "10.0.0.1",
|
||||
MBits: 50,
|
||||
ReservedPorts: []Port{{"main", 80}},
|
||||
ReservedPorts: []Port{{"main", 80, 0}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -243,7 +243,7 @@ func TestAllocsFit_TerminalAlloc_Old(t *testing.T) {
|
|||
Device: "eth0",
|
||||
IP: "10.0.0.1",
|
||||
MBits: 50,
|
||||
ReservedPorts: []Port{{"main", 8000}},
|
||||
ReservedPorts: []Port{{"main", 8000, 0}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -323,7 +323,7 @@ func TestAllocsFit(t *testing.T) {
|
|||
Device: "eth0",
|
||||
IP: "10.0.0.1",
|
||||
MBits: 50,
|
||||
ReservedPorts: []Port{{"main", 8000}},
|
||||
ReservedPorts: []Port{{"main", 8000, 0}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -407,7 +407,7 @@ func TestAllocsFit_TerminalAlloc(t *testing.T) {
|
|||
Device: "eth0",
|
||||
IP: "10.0.0.1",
|
||||
MBits: 50,
|
||||
ReservedPorts: []Port{{"main", 8000}},
|
||||
ReservedPorts: []Port{{"main", 8000, 80}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -113,6 +113,15 @@ func (idx *NetworkIndex) AddAllocs(allocs []*Allocation) (collide bool) {
|
|||
}
|
||||
|
||||
if alloc.AllocatedResources != nil {
|
||||
// Add network resources that are at the task group level
|
||||
if len(alloc.AllocatedResources.Shared.Networks) > 0 {
|
||||
for _, network := range alloc.AllocatedResources.Shared.Networks {
|
||||
if idx.AddReserved(network) {
|
||||
collide = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, task := range alloc.AllocatedResources.Tasks {
|
||||
if len(task.Networks) == 0 {
|
||||
continue
|
||||
|
|
|
@ -15,7 +15,7 @@ func TestNetworkIndex_Overcommitted(t *testing.T) {
|
|||
Device: "eth0",
|
||||
IP: "192.168.0.100",
|
||||
MBits: 505,
|
||||
ReservedPorts: []Port{{"one", 8000}, {"two", 9000}},
|
||||
ReservedPorts: []Port{{"one", 8000, 0}, {"two", 9000, 0}},
|
||||
}
|
||||
collide := idx.AddReserved(reserved)
|
||||
if collide {
|
||||
|
@ -96,7 +96,7 @@ func TestNetworkIndex_AddAllocs(t *testing.T) {
|
|||
Device: "eth0",
|
||||
IP: "192.168.0.100",
|
||||
MBits: 20,
|
||||
ReservedPorts: []Port{{"one", 8000}, {"two", 9000}},
|
||||
ReservedPorts: []Port{{"one", 8000, 0}, {"two", 9000, 0}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -112,7 +112,7 @@ func TestNetworkIndex_AddAllocs(t *testing.T) {
|
|||
Device: "eth0",
|
||||
IP: "192.168.0.100",
|
||||
MBits: 50,
|
||||
ReservedPorts: []Port{{"one", 10000}},
|
||||
ReservedPorts: []Port{{"one", 10000, 0}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -146,7 +146,7 @@ func TestNetworkIndex_AddReserved(t *testing.T) {
|
|||
Device: "eth0",
|
||||
IP: "192.168.0.100",
|
||||
MBits: 20,
|
||||
ReservedPorts: []Port{{"one", 8000}, {"two", 9000}},
|
||||
ReservedPorts: []Port{{"one", 8000, 0}, {"two", 9000, 0}},
|
||||
}
|
||||
collide := idx.AddReserved(reserved)
|
||||
if collide {
|
||||
|
@ -224,7 +224,7 @@ func TestNetworkIndex_AssignNetwork(t *testing.T) {
|
|||
Device: "eth0",
|
||||
IP: "192.168.0.100",
|
||||
MBits: 20,
|
||||
ReservedPorts: []Port{{"one", 8000}, {"two", 9000}},
|
||||
ReservedPorts: []Port{{"one", 8000, 0}, {"two", 9000, 0}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -238,7 +238,7 @@ func TestNetworkIndex_AssignNetwork(t *testing.T) {
|
|||
Device: "eth0",
|
||||
IP: "192.168.0.100",
|
||||
MBits: 50,
|
||||
ReservedPorts: []Port{{"main", 10000}},
|
||||
ReservedPorts: []Port{{"main", 10000, 0}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -249,7 +249,7 @@ func TestNetworkIndex_AssignNetwork(t *testing.T) {
|
|||
|
||||
// Ask for a reserved port
|
||||
ask := &NetworkResource{
|
||||
ReservedPorts: []Port{{"main", 8000}},
|
||||
ReservedPorts: []Port{{"main", 8000, 0}},
|
||||
}
|
||||
offer, err := idx.AssignNetwork(ask)
|
||||
if err != nil {
|
||||
|
@ -261,14 +261,14 @@ func TestNetworkIndex_AssignNetwork(t *testing.T) {
|
|||
if offer.IP != "192.168.0.101" {
|
||||
t.Fatalf("bad: %#v", offer)
|
||||
}
|
||||
rp := Port{"main", 8000}
|
||||
rp := Port{"main", 8000, 0}
|
||||
if len(offer.ReservedPorts) != 1 || offer.ReservedPorts[0] != rp {
|
||||
t.Fatalf("bad: %#v", offer)
|
||||
}
|
||||
|
||||
// Ask for dynamic ports
|
||||
ask = &NetworkResource{
|
||||
DynamicPorts: []Port{{"http", 0}, {"https", 0}, {"admin", 0}},
|
||||
DynamicPorts: []Port{{"http", 0, 80}, {"https", 0, 443}, {"admin", 0, 8080}},
|
||||
}
|
||||
offer, err = idx.AssignNetwork(ask)
|
||||
if err != nil {
|
||||
|
@ -291,8 +291,8 @@ func TestNetworkIndex_AssignNetwork(t *testing.T) {
|
|||
|
||||
// Ask for reserved + dynamic ports
|
||||
ask = &NetworkResource{
|
||||
ReservedPorts: []Port{{"main", 2345}},
|
||||
DynamicPorts: []Port{{"http", 0}, {"https", 0}, {"admin", 0}},
|
||||
ReservedPorts: []Port{{"main", 2345, 0}},
|
||||
DynamicPorts: []Port{{"http", 0, 80}, {"https", 0, 443}, {"admin", 0, 8080}},
|
||||
}
|
||||
offer, err = idx.AssignNetwork(ask)
|
||||
if err != nil {
|
||||
|
@ -305,7 +305,7 @@ func TestNetworkIndex_AssignNetwork(t *testing.T) {
|
|||
t.Fatalf("bad: %#v", offer)
|
||||
}
|
||||
|
||||
rp = Port{"main", 2345}
|
||||
rp = Port{"main", 2345, 0}
|
||||
if len(offer.ReservedPorts) != 1 || offer.ReservedPorts[0] != rp {
|
||||
t.Fatalf("bad: %#v", offer)
|
||||
}
|
||||
|
@ -350,7 +350,7 @@ func TestNetworkIndex_AssignNetwork_Dynamic_Contention(t *testing.T) {
|
|||
|
||||
// Ask for dynamic ports
|
||||
ask := &NetworkResource{
|
||||
DynamicPorts: []Port{{"http", 0}},
|
||||
DynamicPorts: []Port{{"http", 0, 80}},
|
||||
}
|
||||
offer, err := idx.AssignNetwork(ask)
|
||||
if err != nil {
|
||||
|
@ -379,7 +379,7 @@ func TestNetworkIndex_Overcommitted_Old(t *testing.T) {
|
|||
Device: "eth0",
|
||||
IP: "192.168.0.100",
|
||||
MBits: 505,
|
||||
ReservedPorts: []Port{{"one", 8000}, {"two", 9000}},
|
||||
ReservedPorts: []Port{{"one", 8000, 0}, {"two", 9000, 0}},
|
||||
}
|
||||
collide := idx.AddReserved(reserved)
|
||||
if collide {
|
||||
|
@ -431,7 +431,7 @@ func TestNetworkIndex_SetNode_Old(t *testing.T) {
|
|||
{
|
||||
Device: "eth0",
|
||||
IP: "192.168.0.100",
|
||||
ReservedPorts: []Port{{"ssh", 22}},
|
||||
ReservedPorts: []Port{{"ssh", 22, 0}},
|
||||
MBits: 1,
|
||||
},
|
||||
},
|
||||
|
@ -468,7 +468,7 @@ func TestNetworkIndex_AddAllocs_Old(t *testing.T) {
|
|||
Device: "eth0",
|
||||
IP: "192.168.0.100",
|
||||
MBits: 20,
|
||||
ReservedPorts: []Port{{"one", 8000}, {"two", 9000}},
|
||||
ReservedPorts: []Port{{"one", 8000, 0}, {"two", 9000, 0}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -482,7 +482,7 @@ func TestNetworkIndex_AddAllocs_Old(t *testing.T) {
|
|||
Device: "eth0",
|
||||
IP: "192.168.0.100",
|
||||
MBits: 50,
|
||||
ReservedPorts: []Port{{"one", 10000}},
|
||||
ReservedPorts: []Port{{"one", 10000, 0}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -526,7 +526,7 @@ func TestNetworkIndex_yieldIP_Old(t *testing.T) {
|
|||
{
|
||||
Device: "eth0",
|
||||
IP: "192.168.0.100",
|
||||
ReservedPorts: []Port{{"ssh", 22}},
|
||||
ReservedPorts: []Port{{"ssh", 22, 0}},
|
||||
MBits: 1,
|
||||
},
|
||||
},
|
||||
|
@ -565,7 +565,7 @@ func TestNetworkIndex_AssignNetwork_Old(t *testing.T) {
|
|||
{
|
||||
Device: "eth0",
|
||||
IP: "192.168.0.100",
|
||||
ReservedPorts: []Port{{"ssh", 22}},
|
||||
ReservedPorts: []Port{{"ssh", 22, 0}},
|
||||
MBits: 1,
|
||||
},
|
||||
},
|
||||
|
@ -582,7 +582,7 @@ func TestNetworkIndex_AssignNetwork_Old(t *testing.T) {
|
|||
Device: "eth0",
|
||||
IP: "192.168.0.100",
|
||||
MBits: 20,
|
||||
ReservedPorts: []Port{{"one", 8000}, {"two", 9000}},
|
||||
ReservedPorts: []Port{{"one", 8000, 0}, {"two", 9000, 0}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -596,7 +596,7 @@ func TestNetworkIndex_AssignNetwork_Old(t *testing.T) {
|
|||
Device: "eth0",
|
||||
IP: "192.168.0.100",
|
||||
MBits: 50,
|
||||
ReservedPorts: []Port{{"main", 10000}},
|
||||
ReservedPorts: []Port{{"main", 10000, 0}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -607,7 +607,7 @@ func TestNetworkIndex_AssignNetwork_Old(t *testing.T) {
|
|||
|
||||
// Ask for a reserved port
|
||||
ask := &NetworkResource{
|
||||
ReservedPorts: []Port{{"main", 8000}},
|
||||
ReservedPorts: []Port{{"main", 8000, 0}},
|
||||
}
|
||||
offer, err := idx.AssignNetwork(ask)
|
||||
if err != nil {
|
||||
|
@ -619,14 +619,14 @@ func TestNetworkIndex_AssignNetwork_Old(t *testing.T) {
|
|||
if offer.IP != "192.168.0.101" {
|
||||
t.Fatalf("bad: %#v", offer)
|
||||
}
|
||||
rp := Port{"main", 8000}
|
||||
rp := Port{"main", 8000, 0}
|
||||
if len(offer.ReservedPorts) != 1 || offer.ReservedPorts[0] != rp {
|
||||
t.Fatalf("bad: %#v", offer)
|
||||
}
|
||||
|
||||
// Ask for dynamic ports
|
||||
ask = &NetworkResource{
|
||||
DynamicPorts: []Port{{"http", 0}, {"https", 0}, {"admin", 0}},
|
||||
DynamicPorts: []Port{{"http", 0, 80}, {"https", 0, 443}, {"admin", 0, 8080}},
|
||||
}
|
||||
offer, err = idx.AssignNetwork(ask)
|
||||
if err != nil {
|
||||
|
@ -649,8 +649,8 @@ func TestNetworkIndex_AssignNetwork_Old(t *testing.T) {
|
|||
|
||||
// Ask for reserved + dynamic ports
|
||||
ask = &NetworkResource{
|
||||
ReservedPorts: []Port{{"main", 2345}},
|
||||
DynamicPorts: []Port{{"http", 0}, {"https", 0}, {"admin", 0}},
|
||||
ReservedPorts: []Port{{"main", 2345, 0}},
|
||||
DynamicPorts: []Port{{"http", 0, 80}, {"https", 0, 443}, {"admin", 0, 8080}},
|
||||
}
|
||||
offer, err = idx.AssignNetwork(ask)
|
||||
if err != nil {
|
||||
|
@ -663,7 +663,7 @@ func TestNetworkIndex_AssignNetwork_Old(t *testing.T) {
|
|||
t.Fatalf("bad: %#v", offer)
|
||||
}
|
||||
|
||||
rp = Port{"main", 2345}
|
||||
rp = Port{"main", 2345, 0}
|
||||
if len(offer.ReservedPorts) != 1 || offer.ReservedPorts[0] != rp {
|
||||
t.Fatalf("bad: %#v", offer)
|
||||
}
|
||||
|
@ -716,7 +716,7 @@ func TestNetworkIndex_AssignNetwork_Dynamic_Contention_Old(t *testing.T) {
|
|||
|
||||
// Ask for dynamic ports
|
||||
ask := &NetworkResource{
|
||||
DynamicPorts: []Port{{"http", 0}},
|
||||
DynamicPorts: []Port{{"http", 0, 80}},
|
||||
}
|
||||
offer, err := idx.AssignNetwork(ask)
|
||||
if err != nil {
|
||||
|
|
|
@ -1937,13 +1937,7 @@ func (r *Resources) Copy() *Resources {
|
|||
*newR = *r
|
||||
|
||||
// Copy the network objects
|
||||
if r.Networks != nil {
|
||||
n := len(r.Networks)
|
||||
newR.Networks = make([]*NetworkResource, n)
|
||||
for i := 0; i < n; i++ {
|
||||
newR.Networks[i] = r.Networks[i].Copy()
|
||||
}
|
||||
}
|
||||
newR.Networks = r.Networks.Copy()
|
||||
|
||||
// Copy the devices
|
||||
if r.Devices != nil {
|
||||
|
@ -2011,11 +2005,13 @@ func (r *Resources) GoString() string {
|
|||
type Port struct {
|
||||
Label string
|
||||
Value int
|
||||
To int
|
||||
}
|
||||
|
||||
// NetworkResource is used to represent available network
|
||||
// resources
|
||||
type NetworkResource struct {
|
||||
Mode string // Mode of the network
|
||||
Device string // Name of the device
|
||||
CIDR string // CIDR block of addresses
|
||||
IP string // Host IP address
|
||||
|
@ -2025,6 +2021,10 @@ type NetworkResource struct {
|
|||
}
|
||||
|
||||
func (nr *NetworkResource) Equals(other *NetworkResource) bool {
|
||||
if nr.Mode != other.Mode {
|
||||
return false
|
||||
}
|
||||
|
||||
if nr.Device != other.Device {
|
||||
return false
|
||||
}
|
||||
|
@ -2065,6 +2065,7 @@ func (nr *NetworkResource) Equals(other *NetworkResource) bool {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -2137,6 +2138,18 @@ func (n *NetworkResource) PortLabels() map[string]int {
|
|||
// Networks defined for a task on the Resources struct.
|
||||
type Networks []*NetworkResource
|
||||
|
||||
func (ns Networks) Copy() Networks {
|
||||
if len(ns) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]*NetworkResource, len(ns))
|
||||
for i := range ns {
|
||||
out[i] = ns[i].Copy()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Port assignment and IP for the given label or empty values.
|
||||
func (ns Networks) Port(label string) (string, int) {
|
||||
for _, n := range ns {
|
||||
|
@ -2289,13 +2302,7 @@ func (n *NodeResources) Copy() *NodeResources {
|
|||
*newN = *n
|
||||
|
||||
// Copy the networks
|
||||
if n.Networks != nil {
|
||||
networks := len(n.Networks)
|
||||
newN.Networks = make([]*NetworkResource, networks)
|
||||
for i := 0; i < networks; i++ {
|
||||
newN.Networks[i] = n.Networks[i].Copy()
|
||||
}
|
||||
}
|
||||
newN.Networks = n.Networks.Copy()
|
||||
|
||||
// Copy the devices
|
||||
if n.Devices != nil {
|
||||
|
@ -2822,18 +2829,19 @@ func (a *AllocatedResources) Copy() *AllocatedResources {
|
|||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
newA := new(AllocatedResources)
|
||||
*newA = *a
|
||||
|
||||
out := AllocatedResources{
|
||||
Shared: a.Shared.Copy(),
|
||||
}
|
||||
|
||||
if a.Tasks != nil {
|
||||
tr := make(map[string]*AllocatedTaskResources, len(newA.Tasks))
|
||||
for task, resource := range newA.Tasks {
|
||||
tr[task] = resource.Copy()
|
||||
out.Tasks = make(map[string]*AllocatedTaskResources, len(out.Tasks))
|
||||
for task, resource := range a.Tasks {
|
||||
out.Tasks[task] = resource.Copy()
|
||||
}
|
||||
newA.Tasks = tr
|
||||
}
|
||||
|
||||
return newA
|
||||
return &out
|
||||
}
|
||||
|
||||
// Comparable returns a comparable version of the allocations allocated
|
||||
|
@ -2849,6 +2857,13 @@ func (a *AllocatedResources) Comparable() *ComparableResources {
|
|||
for _, r := range a.Tasks {
|
||||
c.Flattened.Add(r)
|
||||
}
|
||||
// Add network resources that are at the task group level
|
||||
for _, network := range a.Shared.Networks {
|
||||
c.Flattened.Add(&AllocatedTaskResources{
|
||||
Networks: []*NetworkResource{network},
|
||||
})
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
|
@ -2882,13 +2897,7 @@ func (a *AllocatedTaskResources) Copy() *AllocatedTaskResources {
|
|||
*newA = *a
|
||||
|
||||
// Copy the networks
|
||||
if a.Networks != nil {
|
||||
n := len(a.Networks)
|
||||
newA.Networks = make([]*NetworkResource, n)
|
||||
for i := 0; i < n; i++ {
|
||||
newA.Networks[i] = a.Networks[i].Copy()
|
||||
}
|
||||
}
|
||||
newA.Networks = a.Networks.Copy()
|
||||
|
||||
// Copy the devices
|
||||
if newA.Devices != nil {
|
||||
|
@ -2970,15 +2979,24 @@ func (a *AllocatedTaskResources) Subtract(delta *AllocatedTaskResources) {
|
|||
|
||||
// AllocatedSharedResources are the set of resources allocated to a task group.
|
||||
type AllocatedSharedResources struct {
|
||||
Networks Networks
|
||||
DiskMB int64
|
||||
}
|
||||
|
||||
func (a AllocatedSharedResources) Copy() AllocatedSharedResources {
|
||||
return AllocatedSharedResources{
|
||||
Networks: a.Networks.Copy(),
|
||||
DiskMB: a.DiskMB,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AllocatedSharedResources) Add(delta *AllocatedSharedResources) {
|
||||
if delta == nil {
|
||||
return
|
||||
}
|
||||
|
||||
a.Networks = append(a.Networks, delta.Networks...)
|
||||
a.DiskMB += delta.DiskMB
|
||||
|
||||
}
|
||||
|
||||
func (a *AllocatedSharedResources) Subtract(delta *AllocatedSharedResources) {
|
||||
|
@ -2986,6 +3004,17 @@ func (a *AllocatedSharedResources) Subtract(delta *AllocatedSharedResources) {
|
|||
return
|
||||
}
|
||||
|
||||
diff := map[*NetworkResource]bool{}
|
||||
for _, n := range delta.Networks {
|
||||
diff[n] = true
|
||||
}
|
||||
var nets Networks
|
||||
for _, n := range a.Networks {
|
||||
if _, ok := diff[n]; !ok {
|
||||
nets = append(nets, n)
|
||||
}
|
||||
}
|
||||
a.Networks = nets
|
||||
a.DiskMB -= delta.DiskMB
|
||||
}
|
||||
|
||||
|
@ -4623,6 +4652,13 @@ type TaskGroup struct {
|
|||
// Spread can be specified at the task group level to express spreading
|
||||
// allocations across a desired attribute, such as datacenter
|
||||
Spreads []*Spread
|
||||
|
||||
// Networks are the network configuration for the task group. This can be
|
||||
// overridden in the task.
|
||||
Networks Networks
|
||||
|
||||
// Services this group provides
|
||||
Services []*Service
|
||||
}
|
||||
|
||||
func (tg *TaskGroup) Copy() *TaskGroup {
|
||||
|
@ -4638,6 +4674,15 @@ func (tg *TaskGroup) Copy() *TaskGroup {
|
|||
ntg.Affinities = CopySliceAffinities(ntg.Affinities)
|
||||
ntg.Spreads = CopySliceSpreads(ntg.Spreads)
|
||||
|
||||
// Copy the network objects
|
||||
if tg.Networks != nil {
|
||||
n := len(tg.Networks)
|
||||
ntg.Networks = make([]*NetworkResource, n)
|
||||
for i := 0; i < n; i++ {
|
||||
ntg.Networks[i] = tg.Networks[i].Copy()
|
||||
}
|
||||
}
|
||||
|
||||
if tg.Tasks != nil {
|
||||
tasks := make([]*Task, len(ntg.Tasks))
|
||||
for i, t := range ntg.Tasks {
|
||||
|
@ -4651,6 +4696,14 @@ func (tg *TaskGroup) Copy() *TaskGroup {
|
|||
if tg.EphemeralDisk != nil {
|
||||
ntg.EphemeralDisk = tg.EphemeralDisk.Copy()
|
||||
}
|
||||
|
||||
if tg.Services != nil {
|
||||
ntg.Services = make([]*Service, len(tg.Services))
|
||||
for i, s := range tg.Services {
|
||||
ntg.Services[i] = s.Copy()
|
||||
}
|
||||
}
|
||||
|
||||
return ntg
|
||||
}
|
||||
|
||||
|
@ -4786,10 +4839,8 @@ func (tg *TaskGroup) Validate(j *Job) error {
|
|||
}
|
||||
}
|
||||
|
||||
// Check for duplicate tasks, that there is only leader task if any,
|
||||
// and no duplicated static ports
|
||||
// Check that there is only one leader task if any
|
||||
tasks := make(map[string]int)
|
||||
staticPorts := make(map[int]string)
|
||||
leaderTasks := 0
|
||||
for idx, task := range tg.Tasks {
|
||||
if task.Name == "" {
|
||||
|
@ -4803,27 +4854,18 @@ func (tg *TaskGroup) Validate(j *Job) error {
|
|||
if task.Leader {
|
||||
leaderTasks++
|
||||
}
|
||||
|
||||
if task.Resources == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, net := range task.Resources.Networks {
|
||||
for _, port := range net.ReservedPorts {
|
||||
if other, ok := staticPorts[port.Value]; ok {
|
||||
err := fmt.Errorf("Static port %d already reserved by %s", port.Value, other)
|
||||
mErr.Errors = append(mErr.Errors, err)
|
||||
} else {
|
||||
staticPorts[port.Value] = fmt.Sprintf("%s:%s", task.Name, port.Label)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if leaderTasks > 1 {
|
||||
mErr.Errors = append(mErr.Errors, fmt.Errorf("Only one task may be marked as leader"))
|
||||
}
|
||||
|
||||
// Validate task group and task network resources
|
||||
if err := tg.validateNetworks(); err != nil {
|
||||
outer := fmt.Errorf("Task group network validation failed: %v", err)
|
||||
mErr.Errors = append(mErr.Errors, outer)
|
||||
}
|
||||
|
||||
// Validate the tasks
|
||||
for _, task := range tg.Tasks {
|
||||
if err := task.Validate(tg.EphemeralDisk, j.Type); err != nil {
|
||||
|
@ -4834,6 +4876,75 @@ func (tg *TaskGroup) Validate(j *Job) error {
|
|||
return mErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
func (tg *TaskGroup) validateNetworks() error {
|
||||
var mErr multierror.Error
|
||||
portLabels := make(map[string]string)
|
||||
staticPorts := make(map[int]string)
|
||||
mappedPorts := make(map[int]string)
|
||||
|
||||
for _, net := range tg.Networks {
|
||||
for _, port := range append(net.ReservedPorts, net.DynamicPorts...) {
|
||||
if other, ok := portLabels[port.Label]; ok {
|
||||
mErr.Errors = append(mErr.Errors, fmt.Errorf("Port label %s already in use by %s", port.Label, other))
|
||||
} else {
|
||||
portLabels[port.Label] = "taskgroup network"
|
||||
}
|
||||
|
||||
if port.Value != 0 {
|
||||
// static port
|
||||
if other, ok := staticPorts[port.Value]; ok {
|
||||
err := fmt.Errorf("Static port %d already reserved by %s", port.Value, other)
|
||||
mErr.Errors = append(mErr.Errors, err)
|
||||
} else {
|
||||
staticPorts[port.Value] = fmt.Sprintf("taskgroup network:%s", port.Label)
|
||||
}
|
||||
}
|
||||
|
||||
if port.To != 0 {
|
||||
if other, ok := mappedPorts[port.To]; ok {
|
||||
err := fmt.Errorf("Port mapped to %d already in use by %s", port.To, other)
|
||||
mErr.Errors = append(mErr.Errors, err)
|
||||
} else {
|
||||
mappedPorts[port.To] = fmt.Sprintf("taskgroup network:%s", port.Label)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check for duplicate tasks or port labels, and no duplicated static or mapped ports
|
||||
for _, task := range tg.Tasks {
|
||||
if task.Resources == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, net := range task.Resources.Networks {
|
||||
for _, port := range append(net.ReservedPorts, net.DynamicPorts...) {
|
||||
if other, ok := portLabels[port.Label]; ok {
|
||||
mErr.Errors = append(mErr.Errors, fmt.Errorf("Port label %s already in use by %s", port.Label, other))
|
||||
}
|
||||
|
||||
if port.Value != 0 {
|
||||
if other, ok := staticPorts[port.Value]; ok {
|
||||
err := fmt.Errorf("Static port %d already reserved by %s", port.Value, other)
|
||||
mErr.Errors = append(mErr.Errors, err)
|
||||
} else {
|
||||
staticPorts[port.Value] = fmt.Sprintf("%s:%s", task.Name, port.Label)
|
||||
}
|
||||
}
|
||||
|
||||
if port.To != 0 {
|
||||
if other, ok := mappedPorts[port.To]; ok {
|
||||
err := fmt.Errorf("Port mapped to %d already in use by %s", port.To, other)
|
||||
mErr.Errors = append(mErr.Errors, err)
|
||||
} else {
|
||||
mappedPorts[port.To] = fmt.Sprintf("taskgroup network:%s", port.Label)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return mErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
// Warnings returns a list of warnings that may be from dubious settings or
|
||||
// deprecation warnings.
|
||||
func (tg *TaskGroup) Warnings(j *Job) error {
|
||||
|
@ -4891,6 +5002,26 @@ func (c *CheckRestart) Copy() *CheckRestart {
|
|||
return nc
|
||||
}
|
||||
|
||||
func (c *CheckRestart) Equals(o *CheckRestart) bool {
|
||||
if c == nil || o == nil {
|
||||
return c == o
|
||||
}
|
||||
|
||||
if c.Limit != o.Limit {
|
||||
return false
|
||||
}
|
||||
|
||||
if c.Grace != o.Grace {
|
||||
return false
|
||||
}
|
||||
|
||||
if c.IgnoreWarnings != o.IgnoreWarnings {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *CheckRestart) Validate() error {
|
||||
if c == nil {
|
||||
return nil
|
||||
|
@ -4958,6 +5089,83 @@ func (sc *ServiceCheck) Copy() *ServiceCheck {
|
|||
return nsc
|
||||
}
|
||||
|
||||
func (sc *ServiceCheck) Equals(o *ServiceCheck) bool {
|
||||
if sc == nil || o == nil {
|
||||
return sc == o
|
||||
}
|
||||
|
||||
if sc.Name != o.Name {
|
||||
return false
|
||||
}
|
||||
|
||||
if sc.AddressMode != o.AddressMode {
|
||||
return false
|
||||
}
|
||||
|
||||
if !helper.CompareSliceSetString(sc.Args, o.Args) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !sc.CheckRestart.Equals(o.CheckRestart) {
|
||||
return false
|
||||
}
|
||||
|
||||
if sc.Command != o.Command {
|
||||
return false
|
||||
}
|
||||
|
||||
if sc.GRPCService != o.GRPCService {
|
||||
return false
|
||||
}
|
||||
|
||||
if sc.GRPCUseTLS != o.GRPCUseTLS {
|
||||
return false
|
||||
}
|
||||
|
||||
// Use DeepEqual here as order of slice values could matter
|
||||
if !reflect.DeepEqual(sc.Header, o.Header) {
|
||||
return false
|
||||
}
|
||||
|
||||
if sc.InitialStatus != o.InitialStatus {
|
||||
return false
|
||||
}
|
||||
|
||||
if sc.Interval != o.Interval {
|
||||
return false
|
||||
}
|
||||
|
||||
if sc.Method != o.Method {
|
||||
return false
|
||||
}
|
||||
|
||||
if sc.Path != o.Path {
|
||||
return false
|
||||
}
|
||||
|
||||
if sc.PortLabel != o.Path {
|
||||
return false
|
||||
}
|
||||
|
||||
if sc.Protocol != o.Protocol {
|
||||
return false
|
||||
}
|
||||
|
||||
if sc.TLSSkipVerify != o.TLSSkipVerify {
|
||||
return false
|
||||
}
|
||||
|
||||
if sc.Timeout != o.Timeout {
|
||||
return false
|
||||
}
|
||||
|
||||
if sc.Type != o.Type {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (sc *ServiceCheck) Canonicalize(serviceName string) {
|
||||
// Ensure empty maps/slices are treated as null to avoid scheduling
|
||||
// issues when using DeepEquals.
|
||||
|
@ -5134,6 +5342,7 @@ type Service struct {
|
|||
Tags []string // List of tags for the service
|
||||
CanaryTags []string // List of tags for the service when it is a canary
|
||||
Checks []*ServiceCheck // List of checks associated with the service
|
||||
Connect *ConsulConnect // Consul Connect configuration
|
||||
}
|
||||
|
||||
func (s *Service) Copy() *Service {
|
||||
|
@ -5260,6 +5469,55 @@ func (s *Service) Hash(allocID, taskName string, canary bool) string {
|
|||
return b32.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func (s *Service) Equals(o *Service) bool {
|
||||
if s == nil || o == nil {
|
||||
return s == o
|
||||
}
|
||||
|
||||
if s.AddressMode != o.AddressMode {
|
||||
return false
|
||||
}
|
||||
|
||||
if !helper.CompareSliceSetString(s.CanaryTags, o.CanaryTags) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(s.Checks) != len(o.Checks) {
|
||||
return false
|
||||
}
|
||||
|
||||
OUTER:
|
||||
for i := range s.Checks {
|
||||
for ii := range o.Checks {
|
||||
if s.Checks[i].Equals(o.Checks[ii]) {
|
||||
// Found match; continue with next check
|
||||
continue OUTER
|
||||
}
|
||||
}
|
||||
|
||||
// No match
|
||||
return false
|
||||
}
|
||||
|
||||
if !s.Connect.Equals(o.Connect) {
|
||||
return false
|
||||
}
|
||||
|
||||
if s.Name != o.Name {
|
||||
return false
|
||||
}
|
||||
|
||||
if s.PortLabel != o.PortLabel {
|
||||
return false
|
||||
}
|
||||
|
||||
if !helper.CompareSliceSetString(s.Tags, o.Tags) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const (
|
||||
// DefaultKillTimeout is the default timeout between signaling a task it
|
||||
// will be killed and killing it.
|
||||
|
@ -7828,7 +8086,7 @@ func (a *Allocation) SetEventDisplayMessages() {
|
|||
}
|
||||
|
||||
// COMPAT(0.11): Remove in 0.11
|
||||
// ComparableResources returns the resouces on the allocation
|
||||
// ComparableResources returns the resources on the allocation
|
||||
// handling upgrade paths. After 0.11 calls to this should be replaced with:
|
||||
// alloc.AllocatedResources.Comparable()
|
||||
func (a *Allocation) ComparableResources() *ComparableResources {
|
||||
|
|
|
@ -846,6 +846,28 @@ func TestTaskGroup_Validate(t *testing.T) {
|
|||
if !strings.Contains(err.Error(), "System jobs should not have a reschedule policy") {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
tg = &TaskGroup{
|
||||
Networks: []*NetworkResource{
|
||||
{
|
||||
DynamicPorts: []Port{{"http", 0, 80}},
|
||||
},
|
||||
},
|
||||
Tasks: []*Task{
|
||||
{
|
||||
Resources: &Resources{
|
||||
Networks: []*NetworkResource{
|
||||
{
|
||||
DynamicPorts: []Port{{"http", 0, 80}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err = tg.Validate(j)
|
||||
require.Contains(t, err.Error(), "Port label http already in use")
|
||||
require.Contains(t, err.Error(), "Port mapped to 80 already in use")
|
||||
}
|
||||
|
||||
func TestTask_Validate(t *testing.T) {
|
||||
|
@ -1856,7 +1878,7 @@ func TestResource_Add(t *testing.T) {
|
|||
{
|
||||
CIDR: "10.0.0.0/8",
|
||||
MBits: 100,
|
||||
ReservedPorts: []Port{{"ssh", 22}},
|
||||
ReservedPorts: []Port{{"ssh", 22, 0}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -1868,7 +1890,7 @@ func TestResource_Add(t *testing.T) {
|
|||
{
|
||||
IP: "10.0.0.1",
|
||||
MBits: 50,
|
||||
ReservedPorts: []Port{{"web", 80}},
|
||||
ReservedPorts: []Port{{"web", 80, 0}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -1886,7 +1908,7 @@ func TestResource_Add(t *testing.T) {
|
|||
{
|
||||
CIDR: "10.0.0.0/8",
|
||||
MBits: 150,
|
||||
ReservedPorts: []Port{{"ssh", 22}, {"web", 80}},
|
||||
ReservedPorts: []Port{{"ssh", 22, 0}, {"web", 80, 0}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -1902,7 +1924,7 @@ func TestResource_Add_Network(t *testing.T) {
|
|||
Networks: []*NetworkResource{
|
||||
{
|
||||
MBits: 50,
|
||||
DynamicPorts: []Port{{"http", 0}, {"https", 0}},
|
||||
DynamicPorts: []Port{{"http", 0, 80}, {"https", 0, 443}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -1910,7 +1932,7 @@ func TestResource_Add_Network(t *testing.T) {
|
|||
Networks: []*NetworkResource{
|
||||
{
|
||||
MBits: 25,
|
||||
DynamicPorts: []Port{{"admin", 0}},
|
||||
DynamicPorts: []Port{{"admin", 0, 8080}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -1928,7 +1950,7 @@ func TestResource_Add_Network(t *testing.T) {
|
|||
Networks: []*NetworkResource{
|
||||
{
|
||||
MBits: 75,
|
||||
DynamicPorts: []Port{{"http", 0}, {"https", 0}, {"admin", 0}},
|
||||
DynamicPorts: []Port{{"http", 0, 80}, {"https", 0, 443}, {"admin", 0, 8080}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -1951,7 +1973,7 @@ func TestComparableResources_Subtract(t *testing.T) {
|
|||
{
|
||||
CIDR: "10.0.0.0/8",
|
||||
MBits: 100,
|
||||
ReservedPorts: []Port{{"ssh", 22}},
|
||||
ReservedPorts: []Port{{"ssh", 22, 0}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1972,7 +1994,7 @@ func TestComparableResources_Subtract(t *testing.T) {
|
|||
{
|
||||
CIDR: "10.0.0.0/8",
|
||||
MBits: 20,
|
||||
ReservedPorts: []Port{{"ssh", 22}},
|
||||
ReservedPorts: []Port{{"ssh", 22, 0}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1994,7 +2016,7 @@ func TestComparableResources_Subtract(t *testing.T) {
|
|||
{
|
||||
CIDR: "10.0.0.0/8",
|
||||
MBits: 100,
|
||||
ReservedPorts: []Port{{"ssh", 22}},
|
||||
ReservedPorts: []Port{{"ssh", 22, 0}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -4040,12 +4062,12 @@ func TestNetworkResourcesEquals(t *testing.T) {
|
|||
{
|
||||
IP: "10.0.0.1",
|
||||
MBits: 50,
|
||||
ReservedPorts: []Port{{"web", 80}},
|
||||
ReservedPorts: []Port{{"web", 80, 0}},
|
||||
},
|
||||
{
|
||||
IP: "10.0.0.1",
|
||||
MBits: 50,
|
||||
ReservedPorts: []Port{{"web", 80}},
|
||||
ReservedPorts: []Port{{"web", 80, 0}},
|
||||
},
|
||||
},
|
||||
true,
|
||||
|
@ -4056,12 +4078,12 @@ func TestNetworkResourcesEquals(t *testing.T) {
|
|||
{
|
||||
IP: "10.0.0.0",
|
||||
MBits: 50,
|
||||
ReservedPorts: []Port{{"web", 80}},
|
||||
ReservedPorts: []Port{{"web", 80, 0}},
|
||||
},
|
||||
{
|
||||
IP: "10.0.0.1",
|
||||
MBits: 50,
|
||||
ReservedPorts: []Port{{"web", 80}},
|
||||
ReservedPorts: []Port{{"web", 80, 0}},
|
||||
},
|
||||
},
|
||||
false,
|
||||
|
@ -4072,12 +4094,12 @@ func TestNetworkResourcesEquals(t *testing.T) {
|
|||
{
|
||||
IP: "10.0.0.1",
|
||||
MBits: 40,
|
||||
ReservedPorts: []Port{{"web", 80}},
|
||||
ReservedPorts: []Port{{"web", 80, 0}},
|
||||
},
|
||||
{
|
||||
IP: "10.0.0.1",
|
||||
MBits: 50,
|
||||
ReservedPorts: []Port{{"web", 80}},
|
||||
ReservedPorts: []Port{{"web", 80, 0}},
|
||||
},
|
||||
},
|
||||
false,
|
||||
|
@ -4088,12 +4110,12 @@ func TestNetworkResourcesEquals(t *testing.T) {
|
|||
{
|
||||
IP: "10.0.0.1",
|
||||
MBits: 50,
|
||||
ReservedPorts: []Port{{"web", 80}},
|
||||
ReservedPorts: []Port{{"web", 80, 0}},
|
||||
},
|
||||
{
|
||||
IP: "10.0.0.1",
|
||||
MBits: 50,
|
||||
ReservedPorts: []Port{{"web", 80}, {"web", 80}},
|
||||
ReservedPorts: []Port{{"web", 80, 0}, {"web", 80, 0}},
|
||||
},
|
||||
},
|
||||
false,
|
||||
|
@ -4104,7 +4126,7 @@ func TestNetworkResourcesEquals(t *testing.T) {
|
|||
{
|
||||
IP: "10.0.0.1",
|
||||
MBits: 50,
|
||||
ReservedPorts: []Port{{"web", 80}},
|
||||
ReservedPorts: []Port{{"web", 80, 0}},
|
||||
},
|
||||
{
|
||||
IP: "10.0.0.1",
|
||||
|
@ -4120,12 +4142,12 @@ func TestNetworkResourcesEquals(t *testing.T) {
|
|||
{
|
||||
IP: "10.0.0.1",
|
||||
MBits: 50,
|
||||
ReservedPorts: []Port{{"web", 80}},
|
||||
ReservedPorts: []Port{{"web", 80, 0}},
|
||||
},
|
||||
{
|
||||
IP: "10.0.0.1",
|
||||
MBits: 50,
|
||||
ReservedPorts: []Port{{"notweb", 80}},
|
||||
ReservedPorts: []Port{{"notweb", 80, 0}},
|
||||
},
|
||||
},
|
||||
false,
|
||||
|
@ -4136,12 +4158,12 @@ func TestNetworkResourcesEquals(t *testing.T) {
|
|||
{
|
||||
IP: "10.0.0.1",
|
||||
MBits: 50,
|
||||
DynamicPorts: []Port{{"web", 80}},
|
||||
DynamicPorts: []Port{{"web", 80, 0}},
|
||||
},
|
||||
{
|
||||
IP: "10.0.0.1",
|
||||
MBits: 50,
|
||||
DynamicPorts: []Port{{"web", 80}, {"web", 80}},
|
||||
DynamicPorts: []Port{{"web", 80, 0}, {"web", 80, 0}},
|
||||
},
|
||||
},
|
||||
false,
|
||||
|
@ -4152,7 +4174,7 @@ func TestNetworkResourcesEquals(t *testing.T) {
|
|||
{
|
||||
IP: "10.0.0.1",
|
||||
MBits: 50,
|
||||
DynamicPorts: []Port{{"web", 80}},
|
||||
DynamicPorts: []Port{{"web", 80, 0}},
|
||||
},
|
||||
{
|
||||
IP: "10.0.0.1",
|
||||
|
@ -4168,12 +4190,12 @@ func TestNetworkResourcesEquals(t *testing.T) {
|
|||
{
|
||||
IP: "10.0.0.1",
|
||||
MBits: 50,
|
||||
DynamicPorts: []Port{{"web", 80}},
|
||||
DynamicPorts: []Port{{"web", 80, 0}},
|
||||
},
|
||||
{
|
||||
IP: "10.0.0.1",
|
||||
MBits: 50,
|
||||
DynamicPorts: []Port{{"notweb", 80}},
|
||||
DynamicPorts: []Port{{"notweb", 80, 0}},
|
||||
},
|
||||
},
|
||||
false,
|
||||
|
|
|
@ -459,3 +459,30 @@ func (d *driverPluginClient) ExecTaskStreamingRaw(ctx context.Context,
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *driverPluginClient) CreateNetwork(allocID string) (*NetworkIsolationSpec, error) {
|
||||
req := &proto.CreateNetworkRequest{
|
||||
AllocId: allocID,
|
||||
}
|
||||
|
||||
resp, err := d.client.CreateNetwork(d.doneCtx, req)
|
||||
if err != nil {
|
||||
return nil, grpcutils.HandleGrpcErr(err, d.doneCtx)
|
||||
}
|
||||
|
||||
return NetworkIsolationSpecFromProto(resp.IsolationSpec), nil
|
||||
}
|
||||
|
||||
func (d *driverPluginClient) DestroyNetwork(allocID string, spec *NetworkIsolationSpec) error {
|
||||
req := &proto.DestroyNetworkRequest{
|
||||
AllocId: allocID,
|
||||
IsolationSpec: NetworkIsolationSpecToProto(spec),
|
||||
}
|
||||
|
||||
_, err := d.client.DestroyNetwork(d.doneCtx, req)
|
||||
if err != nil {
|
||||
return grpcutils.HandleGrpcErr(err, d.doneCtx)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -79,6 +79,14 @@ type ExecOptions struct {
|
|||
ResizeCh <-chan TerminalSize
|
||||
}
|
||||
|
||||
// DriverNetworkManager is the interface with exposes function for creating a
|
||||
// network namespace for which tasks can join. This only needs to be implemented
|
||||
// if the driver MUST create the network namespace
|
||||
type DriverNetworkManager interface {
|
||||
CreateNetwork(allocID string) (*NetworkIsolationSpec, error)
|
||||
DestroyNetwork(allocID string, spec *NetworkIsolationSpec) error
|
||||
}
|
||||
|
||||
// InternalDriverPlugin is an interface that exposes functions that are only
|
||||
// implemented by internal driver plugins.
|
||||
type InternalDriverPlugin interface {
|
||||
|
@ -148,6 +156,45 @@ type Capabilities struct {
|
|||
|
||||
//FSIsolation indicates what kind of filesystem isolation the driver supports.
|
||||
FSIsolation FSIsolation
|
||||
|
||||
//NetIsolationModes lists the set of isolation modes supported by the driver
|
||||
NetIsolationModes []NetIsolationMode
|
||||
|
||||
// MustInitiateNetwork tells Nomad that the driver must create the network
|
||||
// namespace and that the CreateNetwork and DestroyNetwork RPCs are implemented.
|
||||
MustInitiateNetwork bool
|
||||
}
|
||||
|
||||
func (c *Capabilities) HasNetIsolationMode(m NetIsolationMode) bool {
|
||||
for _, mode := range c.NetIsolationModes {
|
||||
if mode == m {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type NetIsolationMode string
|
||||
|
||||
var (
|
||||
// NetIsolationModeHost disables network isolation and uses the host network
|
||||
NetIsolationModeHost = NetIsolationMode("host")
|
||||
|
||||
// NetIsolationModeGroup uses the group network namespace for isolation
|
||||
NetIsolationModeGroup = NetIsolationMode("group")
|
||||
|
||||
// NetIsolationModeTask isolates the network to just the task
|
||||
NetIsolationModeTask = NetIsolationMode("task")
|
||||
|
||||
// NetIsolationModeNone indicates that there is no network to isolate and is
|
||||
// inteded to be used for tasks that the client manages remotely
|
||||
NetIsolationModeNone = NetIsolationMode("none")
|
||||
)
|
||||
|
||||
type NetworkIsolationSpec struct {
|
||||
Mode NetIsolationMode
|
||||
Path string
|
||||
Labels map[string]string
|
||||
}
|
||||
|
||||
type TerminalSize struct {
|
||||
|
@ -171,6 +218,7 @@ type TaskConfig struct {
|
|||
StdoutPath string
|
||||
StderrPath string
|
||||
AllocID string
|
||||
NetworkIsolation *NetworkIsolationSpec
|
||||
}
|
||||
|
||||
func (tc *TaskConfig) Copy() *TaskConfig {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -74,6 +74,14 @@ service Driver {
|
|||
// ExecTaskStreaming executes a command inside the tasks execution context
|
||||
// and streams back results
|
||||
rpc ExecTaskStreaming(stream ExecTaskStreamingRequest) returns (stream ExecTaskStreamingResponse) {}
|
||||
|
||||
// CreateNetwork is implemented when the driver needs to create the network
|
||||
// namespace instead of allowing the Nomad client to do.
|
||||
rpc CreateNetwork(CreateNetworkRequest) returns (CreateNetworkResponse) {}
|
||||
|
||||
// DestroyNetwork destroys a previously created network. This rpc is only
|
||||
// implemented if the driver needs to manage network namespace creation.
|
||||
rpc DestroyNetwork(DestroyNetworkRequest) returns (DestroyNetworkResponse) {}
|
||||
}
|
||||
|
||||
message TaskConfigSchemaRequest {}
|
||||
|
@ -314,6 +322,27 @@ message ExecTaskStreamingResponse {
|
|||
ExitResult result = 4;
|
||||
}
|
||||
|
||||
message CreateNetworkRequest {
|
||||
|
||||
// AllocID of the allocation the network is associated with
|
||||
string alloc_id = 1;
|
||||
}
|
||||
|
||||
message CreateNetworkResponse {
|
||||
|
||||
NetworkIsolationSpec isolation_spec = 1;
|
||||
}
|
||||
|
||||
message DestroyNetworkRequest {
|
||||
|
||||
// AllocID of the allocation the network is associated with
|
||||
string alloc_id = 1;
|
||||
|
||||
NetworkIsolationSpec isolation_spec = 2;
|
||||
}
|
||||
|
||||
message DestroyNetworkResponse {}
|
||||
|
||||
message DriverCapabilities {
|
||||
|
||||
// SendSignals indicates that the driver can send process signals (ex. SIGUSR1)
|
||||
|
@ -331,6 +360,24 @@ message DriverCapabilities {
|
|||
}
|
||||
// FsIsolation indicates what kind of filesystem isolation a driver supports.
|
||||
FSIsolation fs_isolation = 3;
|
||||
|
||||
repeated NetworkIsolationSpec.NetworkIsolationMode network_isolation_modes = 4;
|
||||
|
||||
bool must_create_network = 5;
|
||||
}
|
||||
|
||||
message NetworkIsolationSpec {
|
||||
enum NetworkIsolationMode {
|
||||
HOST = 0;
|
||||
GROUP = 1;
|
||||
TASK = 2;
|
||||
NONE = 3;
|
||||
}
|
||||
NetworkIsolationMode mode = 1;
|
||||
|
||||
string path = 2;
|
||||
|
||||
map<string,string> labels = 3;
|
||||
}
|
||||
|
||||
message TaskConfig {
|
||||
|
@ -384,6 +431,10 @@ message TaskConfig {
|
|||
|
||||
// AllocId is the ID of the associated allocation
|
||||
string alloc_id = 15;
|
||||
|
||||
// NetworkIsolationSpec specifies the configuration for the network namespace
|
||||
// to use for the task. *Only supported on Linux
|
||||
NetworkIsolationSpec network_isolation_spec = 16;
|
||||
}
|
||||
|
||||
message Resources {
|
||||
|
|
|
@ -41,6 +41,8 @@ func (b *driverPluginServer) Capabilities(ctx context.Context, req *proto.Capabi
|
|||
Capabilities: &proto.DriverCapabilities{
|
||||
SendSignals: caps.SendSignals,
|
||||
Exec: caps.Exec,
|
||||
MustCreateNetwork: caps.MustInitiateNetwork,
|
||||
NetworkIsolationModes: []proto.NetworkIsolationSpec_NetworkIsolationMode{},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -54,6 +56,10 @@ func (b *driverPluginServer) Capabilities(ctx context.Context, req *proto.Capabi
|
|||
default:
|
||||
resp.Capabilities.FsIsolation = proto.DriverCapabilities_NONE
|
||||
}
|
||||
|
||||
for _, mode := range caps.NetIsolationModes {
|
||||
resp.Capabilities.NetworkIsolationModes = append(resp.Capabilities.NetworkIsolationModes, netIsolationModeToProto(mode))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
|
@ -374,3 +380,34 @@ func (b *driverPluginServer) TaskEvents(req *proto.TaskEventsRequest, srv proto.
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *driverPluginServer) CreateNetwork(ctx context.Context, req *proto.CreateNetworkRequest) (*proto.CreateNetworkResponse, error) {
|
||||
nm, ok := b.impl.(DriverNetworkManager)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("CreateNetwork RPC not supported by driver")
|
||||
}
|
||||
|
||||
spec, err := nm.CreateNetwork(req.AllocId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &proto.CreateNetworkResponse{
|
||||
IsolationSpec: NetworkIsolationSpecToProto(spec),
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (b *driverPluginServer) DestroyNetwork(ctx context.Context, req *proto.DestroyNetworkRequest) (*proto.DestroyNetworkResponse, error) {
|
||||
nm, ok := b.impl.(DriverNetworkManager)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("DestroyNetwork RPC not supported by driver")
|
||||
}
|
||||
|
||||
err := nm.DestroyNetwork(req.AllocId, NetworkIsolationSpecFromProto(req.IsolationSpec))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &proto.DestroyNetworkResponse{}, nil
|
||||
}
|
||||
|
|
|
@ -195,6 +195,19 @@ type MockDriver struct {
|
|||
SignalTaskF func(string, string) error
|
||||
ExecTaskF func(string, []string, time.Duration) (*drivers.ExecTaskResult, error)
|
||||
ExecTaskStreamingF func(context.Context, string, *drivers.ExecOptions) (*drivers.ExitResult, error)
|
||||
MockNetworkManager
|
||||
}
|
||||
|
||||
type MockNetworkManager struct {
|
||||
CreateNetworkF func(string) (*drivers.NetworkIsolationSpec, error)
|
||||
DestroyNetworkF func(string, *drivers.NetworkIsolationSpec) error
|
||||
}
|
||||
|
||||
func (m *MockNetworkManager) CreateNetwork(id string) (*drivers.NetworkIsolationSpec, error) {
|
||||
return m.CreateNetworkF(id)
|
||||
}
|
||||
func (m *MockNetworkManager) DestroyNetwork(id string, spec *drivers.NetworkIsolationSpec) error {
|
||||
return m.DestroyNetworkF(id, spec)
|
||||
}
|
||||
|
||||
func (d *MockDriver) TaskConfigSchema() (*hclspec.Spec, error) { return d.TaskConfigSchemaF() }
|
||||
|
|
|
@ -571,3 +571,55 @@ func memoryUsageMeasuredFieldsFromProto(fields []proto.MemoryUsage_Fields) []str
|
|||
|
||||
return r
|
||||
}
|
||||
|
||||
func netIsolationModeToProto(mode NetIsolationMode) proto.NetworkIsolationSpec_NetworkIsolationMode {
|
||||
switch mode {
|
||||
case NetIsolationModeHost:
|
||||
return proto.NetworkIsolationSpec_HOST
|
||||
case NetIsolationModeGroup:
|
||||
return proto.NetworkIsolationSpec_GROUP
|
||||
case NetIsolationModeTask:
|
||||
return proto.NetworkIsolationSpec_TASK
|
||||
case NetIsolationModeNone:
|
||||
return proto.NetworkIsolationSpec_NONE
|
||||
default:
|
||||
return proto.NetworkIsolationSpec_HOST
|
||||
}
|
||||
}
|
||||
|
||||
func netIsolationModeFromProto(pb proto.NetworkIsolationSpec_NetworkIsolationMode) NetIsolationMode {
|
||||
switch pb {
|
||||
case proto.NetworkIsolationSpec_HOST:
|
||||
return NetIsolationModeHost
|
||||
case proto.NetworkIsolationSpec_GROUP:
|
||||
return NetIsolationModeGroup
|
||||
case proto.NetworkIsolationSpec_TASK:
|
||||
return NetIsolationModeTask
|
||||
case proto.NetworkIsolationSpec_NONE:
|
||||
return NetIsolationModeNone
|
||||
default:
|
||||
return NetIsolationModeHost
|
||||
}
|
||||
}
|
||||
|
||||
func NetworkIsolationSpecToProto(spec *NetworkIsolationSpec) *proto.NetworkIsolationSpec {
|
||||
if spec == nil {
|
||||
return nil
|
||||
}
|
||||
return &proto.NetworkIsolationSpec{
|
||||
Path: spec.Path,
|
||||
Labels: spec.Labels,
|
||||
Mode: netIsolationModeToProto(spec.Mode),
|
||||
}
|
||||
}
|
||||
|
||||
func NetworkIsolationSpecFromProto(pb *proto.NetworkIsolationSpec) *NetworkIsolationSpec {
|
||||
if pb == nil {
|
||||
return nil
|
||||
}
|
||||
return &NetworkIsolationSpec{
|
||||
Path: pb.Path,
|
||||
Labels: pb.Labels,
|
||||
Mode: netIsolationModeFromProto(pb.Mode),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -489,6 +489,9 @@ func (s *GenericScheduler) computePlacements(destructive, place []placementResul
|
|||
DiskMB: int64(tg.EphemeralDisk.SizeMB),
|
||||
},
|
||||
}
|
||||
if option.AllocResources != nil {
|
||||
resources.Shared.Networks = option.AllocResources.Networks
|
||||
}
|
||||
|
||||
// Create an allocation for this
|
||||
alloc := &structs.Allocation{
|
||||
|
@ -508,6 +511,7 @@ func (s *GenericScheduler) computePlacements(destructive, place []placementResul
|
|||
ClientStatus: structs.AllocClientStatusPending,
|
||||
SharedResources: &structs.Resources{
|
||||
DiskMB: tg.EphemeralDisk.SizeMB,
|
||||
Networks: tg.Networks,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -512,7 +512,7 @@ func TestPreemption(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}),
|
||||
createAlloc(allocIDs[1], lowPrioJob, &structs.Resources{
|
||||
createAllocWithTaskgroupNetwork(allocIDs[1], lowPrioJob, &structs.Resources{
|
||||
CPU: 200,
|
||||
MemoryMB: 256,
|
||||
DiskMB: 4 * 1024,
|
||||
|
@ -520,9 +520,13 @@ func TestPreemption(t *testing.T) {
|
|||
{
|
||||
Device: "eth0",
|
||||
IP: "192.168.0.200",
|
||||
MBits: 500,
|
||||
MBits: 200,
|
||||
},
|
||||
},
|
||||
}, &structs.NetworkResource{
|
||||
Device: "eth0",
|
||||
IP: "192.168.0.201",
|
||||
MBits: 300,
|
||||
}),
|
||||
createAlloc(allocIDs[2], lowPrioJob, &structs.Resources{
|
||||
CPU: 200,
|
||||
|
@ -1379,10 +1383,19 @@ func TestPreemption(t *testing.T) {
|
|||
|
||||
// helper method to create allocations with given jobs and resources
|
||||
func createAlloc(id string, job *structs.Job, resource *structs.Resources) *structs.Allocation {
|
||||
return createAllocWithDevice(id, job, resource, nil)
|
||||
return createAllocInner(id, job, resource, nil, nil)
|
||||
}
|
||||
|
||||
// helper method to create allocation with network at the task group level
|
||||
func createAllocWithTaskgroupNetwork(id string, job *structs.Job, resource *structs.Resources, tgNet *structs.NetworkResource) *structs.Allocation {
|
||||
return createAllocInner(id, job, resource, nil, tgNet)
|
||||
}
|
||||
|
||||
func createAllocWithDevice(id string, job *structs.Job, resource *structs.Resources, allocatedDevices *structs.AllocatedDeviceResource) *structs.Allocation {
|
||||
return createAllocInner(id, job, resource, allocatedDevices, nil)
|
||||
}
|
||||
|
||||
func createAllocInner(id string, job *structs.Job, resource *structs.Resources, allocatedDevices *structs.AllocatedDeviceResource, tgNetwork *structs.NetworkResource) *structs.Allocation {
|
||||
alloc := &structs.Allocation{
|
||||
ID: id,
|
||||
Job: job,
|
||||
|
@ -1413,5 +1426,11 @@ func createAllocWithDevice(id string, job *structs.Job, resource *structs.Resour
|
|||
if allocatedDevices != nil {
|
||||
alloc.AllocatedResources.Tasks["web"].Devices = []*structs.AllocatedDeviceResource{allocatedDevices}
|
||||
}
|
||||
|
||||
if tgNetwork != nil {
|
||||
alloc.AllocatedResources.Shared = structs.AllocatedSharedResources{
|
||||
Networks: []*structs.NetworkResource{tgNetwork},
|
||||
}
|
||||
}
|
||||
return alloc
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ type RankedNode struct {
|
|||
FinalScore float64
|
||||
Scores []float64
|
||||
TaskResources map[string]*structs.AllocatedTaskResources
|
||||
AllocResources *structs.AllocatedSharedResources
|
||||
|
||||
// Allocs is used to cache the proposed allocations on the
|
||||
// node. This can be shared between iterators that require it.
|
||||
|
@ -224,6 +225,59 @@ OUTER:
|
|||
}
|
||||
preemptor.SetPreemptions(currentPreemptions)
|
||||
|
||||
// Check if we need task group network resource
|
||||
if len(iter.taskGroup.Networks) > 0 {
|
||||
ask := iter.taskGroup.Networks[0].Copy()
|
||||
offer, err := netIdx.AssignNetwork(ask)
|
||||
if offer == nil {
|
||||
// If eviction is not enabled, mark this node as exhausted and continue
|
||||
if !iter.evict {
|
||||
iter.ctx.Metrics().ExhaustedNode(option.Node,
|
||||
fmt.Sprintf("network: %s", err))
|
||||
netIdx.Release()
|
||||
continue OUTER
|
||||
}
|
||||
|
||||
// Look for preemptible allocations to satisfy the network resource for this task
|
||||
preemptor.SetCandidates(proposed)
|
||||
|
||||
netPreemptions := preemptor.PreemptForNetwork(ask, netIdx)
|
||||
if netPreemptions == nil {
|
||||
iter.ctx.Logger().Named("binpack").Error("preemption not possible ", "network_resource", ask)
|
||||
netIdx.Release()
|
||||
continue OUTER
|
||||
}
|
||||
allocsToPreempt = append(allocsToPreempt, netPreemptions...)
|
||||
|
||||
// First subtract out preempted allocations
|
||||
proposed = structs.RemoveAllocs(proposed, netPreemptions)
|
||||
|
||||
// Reset the network index and try the offer again
|
||||
netIdx.Release()
|
||||
netIdx = structs.NewNetworkIndex()
|
||||
netIdx.SetNode(option.Node)
|
||||
netIdx.AddAllocs(proposed)
|
||||
|
||||
offer, err = netIdx.AssignNetwork(ask)
|
||||
if offer == nil {
|
||||
iter.ctx.Logger().Named("binpack").Error("unexpected error, unable to create network offer after considering preemption", "error", err)
|
||||
netIdx.Release()
|
||||
continue OUTER
|
||||
}
|
||||
}
|
||||
|
||||
// Reserve this to prevent another task from colliding
|
||||
netIdx.AddReserved(offer)
|
||||
|
||||
// Update the network ask to the offer
|
||||
total.Shared.Networks = []*structs.NetworkResource{offer}
|
||||
option.AllocResources = &structs.AllocatedSharedResources{
|
||||
Networks: []*structs.NetworkResource{offer},
|
||||
DiskMB: int64(iter.taskGroup.EphemeralDisk.SizeMB),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for _, task := range iter.taskGroup.Tasks {
|
||||
// Allocate the resources
|
||||
taskResources := &structs.AllocatedTaskResources{
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
require "github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFeasibleRankIterator(t *testing.T) {
|
||||
|
@ -127,6 +127,246 @@ func TestBinPackIterator_NoExistingAlloc(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Tests bin packing iterator with network resources at task and task group level
|
||||
func TestBinPackIterator_Network_Success(t *testing.T) {
|
||||
_, ctx := testContext(t)
|
||||
nodes := []*RankedNode{
|
||||
{
|
||||
Node: &structs.Node{
|
||||
// Perfect fit
|
||||
NodeResources: &structs.NodeResources{
|
||||
Cpu: structs.NodeCpuResources{
|
||||
CpuShares: 2048,
|
||||
},
|
||||
Memory: structs.NodeMemoryResources{
|
||||
MemoryMB: 2048,
|
||||
},
|
||||
Networks: []*structs.NetworkResource{
|
||||
{
|
||||
Mode: "host",
|
||||
Device: "eth0",
|
||||
CIDR: "192.168.0.100/32",
|
||||
MBits: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
ReservedResources: &structs.NodeReservedResources{
|
||||
Cpu: structs.NodeReservedCpuResources{
|
||||
CpuShares: 1024,
|
||||
},
|
||||
Memory: structs.NodeReservedMemoryResources{
|
||||
MemoryMB: 1024,
|
||||
},
|
||||
Networks: structs.NodeReservedNetworkResources{
|
||||
ReservedHostPorts: "1000-2000",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Node: &structs.Node{
|
||||
// 50% fit
|
||||
NodeResources: &structs.NodeResources{
|
||||
Cpu: structs.NodeCpuResources{
|
||||
CpuShares: 4096,
|
||||
},
|
||||
Memory: structs.NodeMemoryResources{
|
||||
MemoryMB: 4096,
|
||||
},
|
||||
Networks: []*structs.NetworkResource{
|
||||
{
|
||||
Mode: "host",
|
||||
Device: "eth0",
|
||||
CIDR: "192.168.0.100/32",
|
||||
MBits: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
ReservedResources: &structs.NodeReservedResources{
|
||||
Cpu: structs.NodeReservedCpuResources{
|
||||
CpuShares: 1024,
|
||||
},
|
||||
Memory: structs.NodeReservedMemoryResources{
|
||||
MemoryMB: 1024,
|
||||
},
|
||||
Networks: structs.NodeReservedNetworkResources{
|
||||
ReservedHostPorts: "1000-2000",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
static := NewStaticRankIterator(ctx, nodes)
|
||||
|
||||
// Create a task group with networks specified at task and task group level
|
||||
taskGroup := &structs.TaskGroup{
|
||||
EphemeralDisk: &structs.EphemeralDisk{},
|
||||
Tasks: []*structs.Task{
|
||||
{
|
||||
Name: "web",
|
||||
Resources: &structs.Resources{
|
||||
CPU: 1024,
|
||||
MemoryMB: 1024,
|
||||
Networks: []*structs.NetworkResource{
|
||||
{
|
||||
Device: "eth0",
|
||||
MBits: 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Networks: []*structs.NetworkResource{
|
||||
{
|
||||
Device: "eth0",
|
||||
MBits: 500,
|
||||
},
|
||||
},
|
||||
}
|
||||
binp := NewBinPackIterator(ctx, static, false, 0)
|
||||
binp.SetTaskGroup(taskGroup)
|
||||
|
||||
scoreNorm := NewScoreNormalizationIterator(ctx, binp)
|
||||
|
||||
out := collectRanked(scoreNorm)
|
||||
require := require.New(t)
|
||||
|
||||
// We expect both nodes to be eligible to place
|
||||
require.Len(out, 2)
|
||||
require.Equal(out[0], nodes[0])
|
||||
require.Equal(out[1], nodes[1])
|
||||
|
||||
// First node should have a perfect score
|
||||
require.Equal(1.0, out[0].FinalScore)
|
||||
|
||||
if out[1].FinalScore < 0.75 || out[1].FinalScore > 0.95 {
|
||||
t.Fatalf("Bad Score: %v", out[1].FinalScore)
|
||||
}
|
||||
|
||||
// Verify network information at taskgroup level
|
||||
require.Equal(500, out[0].AllocResources.Networks[0].MBits)
|
||||
require.Equal(500, out[1].AllocResources.Networks[0].MBits)
|
||||
|
||||
// Verify network information at task level
|
||||
require.Equal(300, out[0].TaskResources["web"].Networks[0].MBits)
|
||||
require.Equal(300, out[1].TaskResources["web"].Networks[0].MBits)
|
||||
}
|
||||
|
||||
// Tests that bin packing iterator fails due to overprovisioning of network
|
||||
// This test has network resources at task group and task level
|
||||
func TestBinPackIterator_Network_Failure(t *testing.T) {
|
||||
_, ctx := testContext(t)
|
||||
nodes := []*RankedNode{
|
||||
{
|
||||
Node: &structs.Node{
|
||||
// 50% fit
|
||||
NodeResources: &structs.NodeResources{
|
||||
Cpu: structs.NodeCpuResources{
|
||||
CpuShares: 4096,
|
||||
},
|
||||
Memory: structs.NodeMemoryResources{
|
||||
MemoryMB: 4096,
|
||||
},
|
||||
Networks: []*structs.NetworkResource{
|
||||
{
|
||||
Mode: "host",
|
||||
Device: "eth0",
|
||||
CIDR: "192.168.0.100/32",
|
||||
MBits: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
ReservedResources: &structs.NodeReservedResources{
|
||||
Cpu: structs.NodeReservedCpuResources{
|
||||
CpuShares: 1024,
|
||||
},
|
||||
Memory: structs.NodeReservedMemoryResources{
|
||||
MemoryMB: 1024,
|
||||
},
|
||||
Networks: structs.NodeReservedNetworkResources{
|
||||
ReservedHostPorts: "1000-2000",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Add a planned alloc that takes up some network mbits at task and task group level
|
||||
plan := ctx.Plan()
|
||||
plan.NodeAllocation[nodes[0].Node.ID] = []*structs.Allocation{
|
||||
{
|
||||
AllocatedResources: &structs.AllocatedResources{
|
||||
Tasks: map[string]*structs.AllocatedTaskResources{
|
||||
"web": {
|
||||
Cpu: structs.AllocatedCpuResources{
|
||||
CpuShares: 2048,
|
||||
},
|
||||
Memory: structs.AllocatedMemoryResources{
|
||||
MemoryMB: 2048,
|
||||
},
|
||||
Networks: []*structs.NetworkResource{
|
||||
{
|
||||
Device: "eth0",
|
||||
IP: "192.168.0.1",
|
||||
MBits: 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Shared: structs.AllocatedSharedResources{
|
||||
Networks: []*structs.NetworkResource{
|
||||
{
|
||||
Device: "eth0",
|
||||
IP: "192.168.0.1",
|
||||
MBits: 400,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
static := NewStaticRankIterator(ctx, nodes)
|
||||
|
||||
// Create a task group with networks specified at task and task group level
|
||||
taskGroup := &structs.TaskGroup{
|
||||
EphemeralDisk: &structs.EphemeralDisk{},
|
||||
Tasks: []*structs.Task{
|
||||
{
|
||||
Name: "web",
|
||||
Resources: &structs.Resources{
|
||||
CPU: 1024,
|
||||
MemoryMB: 1024,
|
||||
Networks: []*structs.NetworkResource{
|
||||
{
|
||||
Device: "eth0",
|
||||
MBits: 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Networks: []*structs.NetworkResource{
|
||||
{
|
||||
Device: "eth0",
|
||||
MBits: 250,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
binp := NewBinPackIterator(ctx, static, false, 0)
|
||||
binp.SetTaskGroup(taskGroup)
|
||||
|
||||
scoreNorm := NewScoreNormalizationIterator(ctx, binp)
|
||||
|
||||
out := collectRanked(scoreNorm)
|
||||
require := require.New(t)
|
||||
|
||||
// We expect a placement failure because we need 800 mbits of network
|
||||
// and only 300 is free
|
||||
require.Len(out, 0)
|
||||
require.Equal(1, ctx.metrics.DimensionExhausted["network: bandwidth exceeded"])
|
||||
}
|
||||
|
||||
func TestBinPackIterator_PlannedAlloc(t *testing.T) {
|
||||
_, ctx := testContext(t)
|
||||
nodes := []*RankedNode{
|
||||
|
|
|
@ -344,6 +344,10 @@ func (s *SystemScheduler) computePlacements(place []allocTuple) error {
|
|||
},
|
||||
}
|
||||
|
||||
if option.AllocResources != nil {
|
||||
resources.Shared.Networks = option.AllocResources.Networks
|
||||
}
|
||||
|
||||
// Create an allocation for this
|
||||
alloc := &structs.Allocation{
|
||||
ID: uuid.Generate(),
|
||||
|
@ -361,6 +365,7 @@ func (s *SystemScheduler) computePlacements(place []allocTuple) error {
|
|||
ClientStatus: structs.AllocClientStatusPending,
|
||||
SharedResources: &structs.Resources{
|
||||
DiskMB: missing.TaskGroup.EphemeralDisk.SizeMB,
|
||||
Networks: missing.TaskGroup.Networks,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -351,6 +351,11 @@ func tasksUpdated(jobA, jobB *structs.Job, taskGroup string) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// Check that the network resources haven't changed
|
||||
if networkUpdated(a.Networks, b.Networks) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check each task
|
||||
for _, at := range a.Tasks {
|
||||
bt := b.LookupTask(at.Name)
|
||||
|
@ -387,12 +392,27 @@ func tasksUpdated(jobA, jobB *structs.Job, taskGroup string) bool {
|
|||
}
|
||||
|
||||
// Inspect the network to see if the dynamic ports are different
|
||||
if len(at.Resources.Networks) != len(bt.Resources.Networks) {
|
||||
if networkUpdated(at.Resources.Networks, bt.Resources.Networks) {
|
||||
return true
|
||||
}
|
||||
for idx := range at.Resources.Networks {
|
||||
an := at.Resources.Networks[idx]
|
||||
bn := bt.Resources.Networks[idx]
|
||||
|
||||
// Inspect the non-network resources
|
||||
if ar, br := at.Resources, bt.Resources; ar.CPU != br.CPU {
|
||||
return true
|
||||
} else if ar.MemoryMB != br.MemoryMB {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func networkUpdated(netA, netB []*structs.NetworkResource) bool {
|
||||
if len(netA) != len(netB) {
|
||||
return true
|
||||
}
|
||||
for idx := range netA {
|
||||
an := netA[idx]
|
||||
bn := netB[idx]
|
||||
|
||||
if an.MBits != bn.MBits {
|
||||
return true
|
||||
|
@ -403,14 +423,6 @@ func tasksUpdated(jobA, jobB *structs.Job, taskGroup string) bool {
|
|||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Inspect the non-network resources
|
||||
if ar, br := at.Resources, bt.Resources; ar.CPU != br.CPU {
|
||||
return true
|
||||
} else if ar.MemoryMB != br.MemoryMB {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -826,8 +838,10 @@ func genericAllocUpdateFn(ctx Context, stack Stack, evalID string) allocUpdateTy
|
|||
Tasks: option.TaskResources,
|
||||
Shared: structs.AllocatedSharedResources{
|
||||
DiskMB: int64(newTG.EphemeralDisk.SizeMB),
|
||||
Networks: newTG.Networks,
|
||||
},
|
||||
}
|
||||
|
||||
// Use metrics from existing alloc for in place upgrade
|
||||
// This is because if the inplace upgrade succeeded, any scoring metadata from
|
||||
// when it first went through the scheduler should still be preserved. Using scoring
|
||||
|
|
|
@ -0,0 +1,497 @@
|
|||
// Copyright 2015 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package libcni
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/invoke"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
)
|
||||
|
||||
var (
|
||||
CacheDir = "/var/lib/cni"
|
||||
)
|
||||
|
||||
// A RuntimeConf holds the arguments to one invocation of a CNI plugin
|
||||
// excepting the network configuration, with the nested exception that
|
||||
// the `runtimeConfig` from the network configuration is included
|
||||
// here.
|
||||
type RuntimeConf struct {
|
||||
ContainerID string
|
||||
NetNS string
|
||||
IfName string
|
||||
Args [][2]string
|
||||
// A dictionary of capability-specific data passed by the runtime
|
||||
// to plugins as top-level keys in the 'runtimeConfig' dictionary
|
||||
// of the plugin's stdin data. libcni will ensure that only keys
|
||||
// in this map which match the capabilities of the plugin are passed
|
||||
// to the plugin
|
||||
CapabilityArgs map[string]interface{}
|
||||
|
||||
// A cache directory in which to library data. Defaults to CacheDir
|
||||
CacheDir string
|
||||
}
|
||||
|
||||
type NetworkConfig struct {
|
||||
Network *types.NetConf
|
||||
Bytes []byte
|
||||
}
|
||||
|
||||
type NetworkConfigList struct {
|
||||
Name string
|
||||
CNIVersion string
|
||||
DisableCheck bool
|
||||
Plugins []*NetworkConfig
|
||||
Bytes []byte
|
||||
}
|
||||
|
||||
type CNI interface {
|
||||
AddNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
|
||||
CheckNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error
|
||||
DelNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error
|
||||
GetNetworkListCachedResult(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
|
||||
|
||||
AddNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
|
||||
CheckNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error
|
||||
DelNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error
|
||||
GetNetworkCachedResult(net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
|
||||
|
||||
ValidateNetworkList(ctx context.Context, net *NetworkConfigList) ([]string, error)
|
||||
ValidateNetwork(ctx context.Context, net *NetworkConfig) ([]string, error)
|
||||
}
|
||||
|
||||
type CNIConfig struct {
|
||||
Path []string
|
||||
exec invoke.Exec
|
||||
}
|
||||
|
||||
// CNIConfig implements the CNI interface
|
||||
var _ CNI = &CNIConfig{}
|
||||
|
||||
// NewCNIConfig returns a new CNIConfig object that will search for plugins
|
||||
// in the given paths and use the given exec interface to run those plugins,
|
||||
// or if the exec interface is not given, will use a default exec handler.
|
||||
func NewCNIConfig(path []string, exec invoke.Exec) *CNIConfig {
|
||||
return &CNIConfig{
|
||||
Path: path,
|
||||
exec: exec,
|
||||
}
|
||||
}
|
||||
|
||||
func buildOneConfig(name, cniVersion string, orig *NetworkConfig, prevResult types.Result, rt *RuntimeConf) (*NetworkConfig, error) {
|
||||
var err error
|
||||
|
||||
inject := map[string]interface{}{
|
||||
"name": name,
|
||||
"cniVersion": cniVersion,
|
||||
}
|
||||
// Add previous plugin result
|
||||
if prevResult != nil {
|
||||
inject["prevResult"] = prevResult
|
||||
}
|
||||
|
||||
// Ensure every config uses the same name and version
|
||||
orig, err = InjectConf(orig, inject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return injectRuntimeConfig(orig, rt)
|
||||
}
|
||||
|
||||
// This function takes a libcni RuntimeConf structure and injects values into
|
||||
// a "runtimeConfig" dictionary in the CNI network configuration JSON that
|
||||
// will be passed to the plugin on stdin.
|
||||
//
|
||||
// Only "capabilities arguments" passed by the runtime are currently injected.
|
||||
// These capabilities arguments are filtered through the plugin's advertised
|
||||
// capabilities from its config JSON, and any keys in the CapabilityArgs
|
||||
// matching plugin capabilities are added to the "runtimeConfig" dictionary
|
||||
// sent to the plugin via JSON on stdin. For example, if the plugin's
|
||||
// capabilities include "portMappings", and the CapabilityArgs map includes a
|
||||
// "portMappings" key, that key and its value are added to the "runtimeConfig"
|
||||
// dictionary to be passed to the plugin's stdin.
|
||||
func injectRuntimeConfig(orig *NetworkConfig, rt *RuntimeConf) (*NetworkConfig, error) {
|
||||
var err error
|
||||
|
||||
rc := make(map[string]interface{})
|
||||
for capability, supported := range orig.Network.Capabilities {
|
||||
if !supported {
|
||||
continue
|
||||
}
|
||||
if data, ok := rt.CapabilityArgs[capability]; ok {
|
||||
rc[capability] = data
|
||||
}
|
||||
}
|
||||
|
||||
if len(rc) > 0 {
|
||||
orig, err = InjectConf(orig, map[string]interface{}{"runtimeConfig": rc})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return orig, nil
|
||||
}
|
||||
|
||||
// ensure we have a usable exec if the CNIConfig was not given one
|
||||
func (c *CNIConfig) ensureExec() invoke.Exec {
|
||||
if c.exec == nil {
|
||||
c.exec = &invoke.DefaultExec{
|
||||
RawExec: &invoke.RawExec{Stderr: os.Stderr},
|
||||
PluginDecoder: version.PluginDecoder{},
|
||||
}
|
||||
}
|
||||
return c.exec
|
||||
}
|
||||
|
||||
func getResultCacheFilePath(netName string, rt *RuntimeConf) string {
|
||||
cacheDir := rt.CacheDir
|
||||
if cacheDir == "" {
|
||||
cacheDir = CacheDir
|
||||
}
|
||||
return filepath.Join(cacheDir, "results", fmt.Sprintf("%s-%s-%s", netName, rt.ContainerID, rt.IfName))
|
||||
}
|
||||
|
||||
func setCachedResult(result types.Result, netName string, rt *RuntimeConf) error {
|
||||
data, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fname := getResultCacheFilePath(netName, rt)
|
||||
if err := os.MkdirAll(filepath.Dir(fname), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(fname, data, 0600)
|
||||
}
|
||||
|
||||
func delCachedResult(netName string, rt *RuntimeConf) error {
|
||||
fname := getResultCacheFilePath(netName, rt)
|
||||
return os.Remove(fname)
|
||||
}
|
||||
|
||||
func getCachedResult(netName, cniVersion string, rt *RuntimeConf) (types.Result, error) {
|
||||
fname := getResultCacheFilePath(netName, rt)
|
||||
data, err := ioutil.ReadFile(fname)
|
||||
if err != nil {
|
||||
// Ignore read errors; the cached result may not exist on-disk
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Read the version of the cached result
|
||||
decoder := version.ConfigDecoder{}
|
||||
resultCniVersion, err := decoder.Decode(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ensure we can understand the result
|
||||
result, err := version.NewResult(resultCniVersion, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert to the config version to ensure plugins get prevResult
|
||||
// in the same version as the config. The cached result version
|
||||
// should match the config version unless the config was changed
|
||||
// while the container was running.
|
||||
result, err = result.GetAsVersion(cniVersion)
|
||||
if err != nil && resultCniVersion != cniVersion {
|
||||
return nil, fmt.Errorf("failed to convert cached result version %q to config version %q: %v", resultCniVersion, cniVersion, err)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
// GetNetworkListCachedResult returns the cached Result of the previous
|
||||
// previous AddNetworkList() operation for a network list, or an error.
|
||||
func (c *CNIConfig) GetNetworkListCachedResult(list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) {
|
||||
return getCachedResult(list.Name, list.CNIVersion, rt)
|
||||
}
|
||||
|
||||
// GetNetworkCachedResult returns the cached Result of the previous
|
||||
// previous AddNetwork() operation for a network, or an error.
|
||||
func (c *CNIConfig) GetNetworkCachedResult(net *NetworkConfig, rt *RuntimeConf) (types.Result, error) {
|
||||
return getCachedResult(net.Network.Name, net.Network.CNIVersion, rt)
|
||||
}
|
||||
|
||||
func (c *CNIConfig) addNetwork(ctx context.Context, name, cniVersion string, net *NetworkConfig, prevResult types.Result, rt *RuntimeConf) (types.Result, error) {
|
||||
c.ensureExec()
|
||||
pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newConf, err := buildOneConfig(name, cniVersion, net, prevResult, rt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return invoke.ExecPluginWithResult(ctx, pluginPath, newConf.Bytes, c.args("ADD", rt), c.exec)
|
||||
}
|
||||
|
||||
// AddNetworkList executes a sequence of plugins with the ADD command
|
||||
func (c *CNIConfig) AddNetworkList(ctx context.Context, list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) {
|
||||
var err error
|
||||
var result types.Result
|
||||
for _, net := range list.Plugins {
|
||||
result, err = c.addNetwork(ctx, list.Name, list.CNIVersion, net, result, rt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err = setCachedResult(result, list.Name, rt); err != nil {
|
||||
return nil, fmt.Errorf("failed to set network %q cached result: %v", list.Name, err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *CNIConfig) checkNetwork(ctx context.Context, name, cniVersion string, net *NetworkConfig, prevResult types.Result, rt *RuntimeConf) error {
|
||||
c.ensureExec()
|
||||
pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newConf, err := buildOneConfig(name, cniVersion, net, prevResult, rt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return invoke.ExecPluginWithoutResult(ctx, pluginPath, newConf.Bytes, c.args("CHECK", rt), c.exec)
|
||||
}
|
||||
|
||||
// CheckNetworkList executes a sequence of plugins with the CHECK command
|
||||
func (c *CNIConfig) CheckNetworkList(ctx context.Context, list *NetworkConfigList, rt *RuntimeConf) error {
|
||||
// CHECK was added in CNI spec version 0.4.0 and higher
|
||||
if gtet, err := version.GreaterThanOrEqualTo(list.CNIVersion, "0.4.0"); err != nil {
|
||||
return err
|
||||
} else if !gtet {
|
||||
return fmt.Errorf("configuration version %q does not support the CHECK command", list.CNIVersion)
|
||||
}
|
||||
|
||||
if list.DisableCheck {
|
||||
return nil
|
||||
}
|
||||
|
||||
cachedResult, err := getCachedResult(list.Name, list.CNIVersion, rt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get network %q cached result: %v", list.Name, err)
|
||||
}
|
||||
|
||||
for _, net := range list.Plugins {
|
||||
if err := c.checkNetwork(ctx, list.Name, list.CNIVersion, net, cachedResult, rt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CNIConfig) delNetwork(ctx context.Context, name, cniVersion string, net *NetworkConfig, prevResult types.Result, rt *RuntimeConf) error {
|
||||
c.ensureExec()
|
||||
pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newConf, err := buildOneConfig(name, cniVersion, net, prevResult, rt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return invoke.ExecPluginWithoutResult(ctx, pluginPath, newConf.Bytes, c.args("DEL", rt), c.exec)
|
||||
}
|
||||
|
||||
// DelNetworkList executes a sequence of plugins with the DEL command
|
||||
func (c *CNIConfig) DelNetworkList(ctx context.Context, list *NetworkConfigList, rt *RuntimeConf) error {
|
||||
var cachedResult types.Result
|
||||
|
||||
// Cached result on DEL was added in CNI spec version 0.4.0 and higher
|
||||
if gtet, err := version.GreaterThanOrEqualTo(list.CNIVersion, "0.4.0"); err != nil {
|
||||
return err
|
||||
} else if gtet {
|
||||
cachedResult, err = getCachedResult(list.Name, list.CNIVersion, rt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get network %q cached result: %v", list.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
for i := len(list.Plugins) - 1; i >= 0; i-- {
|
||||
net := list.Plugins[i]
|
||||
if err := c.delNetwork(ctx, list.Name, list.CNIVersion, net, cachedResult, rt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_ = delCachedResult(list.Name, rt)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddNetwork executes the plugin with the ADD command
|
||||
func (c *CNIConfig) AddNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) (types.Result, error) {
|
||||
result, err := c.addNetwork(ctx, net.Network.Name, net.Network.CNIVersion, net, nil, rt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = setCachedResult(result, net.Network.Name, rt); err != nil {
|
||||
return nil, fmt.Errorf("failed to set network %q cached result: %v", net.Network.Name, err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CheckNetwork executes the plugin with the CHECK command
|
||||
func (c *CNIConfig) CheckNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error {
|
||||
// CHECK was added in CNI spec version 0.4.0 and higher
|
||||
if gtet, err := version.GreaterThanOrEqualTo(net.Network.CNIVersion, "0.4.0"); err != nil {
|
||||
return err
|
||||
} else if !gtet {
|
||||
return fmt.Errorf("configuration version %q does not support the CHECK command", net.Network.CNIVersion)
|
||||
}
|
||||
|
||||
cachedResult, err := getCachedResult(net.Network.Name, net.Network.CNIVersion, rt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get network %q cached result: %v", net.Network.Name, err)
|
||||
}
|
||||
return c.checkNetwork(ctx, net.Network.Name, net.Network.CNIVersion, net, cachedResult, rt)
|
||||
}
|
||||
|
||||
// DelNetwork executes the plugin with the DEL command
|
||||
func (c *CNIConfig) DelNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error {
|
||||
var cachedResult types.Result
|
||||
|
||||
// Cached result on DEL was added in CNI spec version 0.4.0 and higher
|
||||
if gtet, err := version.GreaterThanOrEqualTo(net.Network.CNIVersion, "0.4.0"); err != nil {
|
||||
return err
|
||||
} else if gtet {
|
||||
cachedResult, err = getCachedResult(net.Network.Name, net.Network.CNIVersion, rt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get network %q cached result: %v", net.Network.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.delNetwork(ctx, net.Network.Name, net.Network.CNIVersion, net, cachedResult, rt); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = delCachedResult(net.Network.Name, rt)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateNetworkList checks that a configuration is reasonably valid.
|
||||
// - all the specified plugins exist on disk
|
||||
// - every plugin supports the desired version.
|
||||
//
|
||||
// Returns a list of all capabilities supported by the configuration, or error
|
||||
func (c *CNIConfig) ValidateNetworkList(ctx context.Context, list *NetworkConfigList) ([]string, error) {
|
||||
version := list.CNIVersion
|
||||
|
||||
// holding map for seen caps (in case of duplicates)
|
||||
caps := map[string]interface{}{}
|
||||
|
||||
errs := []error{}
|
||||
for _, net := range list.Plugins {
|
||||
if err := c.validatePlugin(ctx, net.Network.Type, version); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
for c, enabled := range net.Network.Capabilities {
|
||||
if !enabled {
|
||||
continue
|
||||
}
|
||||
caps[c] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return nil, fmt.Errorf("%v", errs)
|
||||
}
|
||||
|
||||
// make caps list
|
||||
cc := make([]string, 0, len(caps))
|
||||
for c := range caps {
|
||||
cc = append(cc, c)
|
||||
}
|
||||
|
||||
return cc, nil
|
||||
}
|
||||
|
||||
// ValidateNetwork checks that a configuration is reasonably valid.
|
||||
// It uses the same logic as ValidateNetworkList)
|
||||
// Returns a list of capabilities
|
||||
func (c *CNIConfig) ValidateNetwork(ctx context.Context, net *NetworkConfig) ([]string, error) {
|
||||
caps := []string{}
|
||||
for c, ok := range net.Network.Capabilities {
|
||||
if ok {
|
||||
caps = append(caps, c)
|
||||
}
|
||||
}
|
||||
if err := c.validatePlugin(ctx, net.Network.Type, net.Network.CNIVersion); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return caps, nil
|
||||
}
|
||||
|
||||
// validatePlugin checks that an individual plugin's configuration is sane
|
||||
func (c *CNIConfig) validatePlugin(ctx context.Context, pluginName, expectedVersion string) error {
|
||||
pluginPath, err := invoke.FindInPath(pluginName, c.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vi, err := invoke.GetVersionInfo(ctx, pluginPath, c.exec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, vers := range vi.SupportedVersions() {
|
||||
if vers == expectedVersion {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("plugin %s does not support config version %q", pluginName, expectedVersion)
|
||||
}
|
||||
|
||||
// GetVersionInfo reports which versions of the CNI spec are supported by
|
||||
// the given plugin.
|
||||
func (c *CNIConfig) GetVersionInfo(ctx context.Context, pluginType string) (version.PluginInfo, error) {
|
||||
c.ensureExec()
|
||||
pluginPath, err := c.exec.FindInPath(pluginType, c.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return invoke.GetVersionInfo(ctx, pluginPath, c.exec)
|
||||
}
|
||||
|
||||
// =====
|
||||
func (c *CNIConfig) args(action string, rt *RuntimeConf) *invoke.Args {
|
||||
return &invoke.Args{
|
||||
Command: action,
|
||||
ContainerID: rt.ContainerID,
|
||||
NetNS: rt.NetNS,
|
||||
PluginArgs: rt.Args,
|
||||
IfName: rt.IfName,
|
||||
Path: strings.Join(c.Path, string(os.PathListSeparator)),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,268 @@
|
|||
// Copyright 2015 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package libcni
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type NotFoundError struct {
|
||||
Dir string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (e NotFoundError) Error() string {
|
||||
return fmt.Sprintf(`no net configuration with name "%s" in %s`, e.Name, e.Dir)
|
||||
}
|
||||
|
||||
type NoConfigsFoundError struct {
|
||||
Dir string
|
||||
}
|
||||
|
||||
func (e NoConfigsFoundError) Error() string {
|
||||
return fmt.Sprintf(`no net configurations found in %s`, e.Dir)
|
||||
}
|
||||
|
||||
func ConfFromBytes(bytes []byte) (*NetworkConfig, error) {
|
||||
conf := &NetworkConfig{Bytes: bytes}
|
||||
if err := json.Unmarshal(bytes, &conf.Network); err != nil {
|
||||
return nil, fmt.Errorf("error parsing configuration: %s", err)
|
||||
}
|
||||
if conf.Network.Type == "" {
|
||||
return nil, fmt.Errorf("error parsing configuration: missing 'type'")
|
||||
}
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
func ConfFromFile(filename string) (*NetworkConfig, error) {
|
||||
bytes, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading %s: %s", filename, err)
|
||||
}
|
||||
return ConfFromBytes(bytes)
|
||||
}
|
||||
|
||||
func ConfListFromBytes(bytes []byte) (*NetworkConfigList, error) {
|
||||
rawList := make(map[string]interface{})
|
||||
if err := json.Unmarshal(bytes, &rawList); err != nil {
|
||||
return nil, fmt.Errorf("error parsing configuration list: %s", err)
|
||||
}
|
||||
|
||||
rawName, ok := rawList["name"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error parsing configuration list: no name")
|
||||
}
|
||||
name, ok := rawName.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error parsing configuration list: invalid name type %T", rawName)
|
||||
}
|
||||
|
||||
var cniVersion string
|
||||
rawVersion, ok := rawList["cniVersion"]
|
||||
if ok {
|
||||
cniVersion, ok = rawVersion.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error parsing configuration list: invalid cniVersion type %T", rawVersion)
|
||||
}
|
||||
}
|
||||
|
||||
disableCheck := false
|
||||
if rawDisableCheck, ok := rawList["disableCheck"]; ok {
|
||||
disableCheck, ok = rawDisableCheck.(bool)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error parsing configuration list: invalid disableCheck type %T", rawDisableCheck)
|
||||
}
|
||||
}
|
||||
|
||||
list := &NetworkConfigList{
|
||||
Name: name,
|
||||
DisableCheck: disableCheck,
|
||||
CNIVersion: cniVersion,
|
||||
Bytes: bytes,
|
||||
}
|
||||
|
||||
var plugins []interface{}
|
||||
plug, ok := rawList["plugins"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error parsing configuration list: no 'plugins' key")
|
||||
}
|
||||
plugins, ok = plug.([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error parsing configuration list: invalid 'plugins' type %T", plug)
|
||||
}
|
||||
if len(plugins) == 0 {
|
||||
return nil, fmt.Errorf("error parsing configuration list: no plugins in list")
|
||||
}
|
||||
|
||||
for i, conf := range plugins {
|
||||
newBytes, err := json.Marshal(conf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to marshal plugin config %d: %v", i, err)
|
||||
}
|
||||
netConf, err := ConfFromBytes(newBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to parse plugin config %d: %v", i, err)
|
||||
}
|
||||
list.Plugins = append(list.Plugins, netConf)
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func ConfListFromFile(filename string) (*NetworkConfigList, error) {
|
||||
bytes, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading %s: %s", filename, err)
|
||||
}
|
||||
return ConfListFromBytes(bytes)
|
||||
}
|
||||
|
||||
func ConfFiles(dir string, extensions []string) ([]string, error) {
|
||||
// In part, adapted from rkt/networking/podenv.go#listFiles
|
||||
files, err := ioutil.ReadDir(dir)
|
||||
switch {
|
||||
case err == nil: // break
|
||||
case os.IsNotExist(err):
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
confFiles := []string{}
|
||||
for _, f := range files {
|
||||
if f.IsDir() {
|
||||
continue
|
||||
}
|
||||
fileExt := filepath.Ext(f.Name())
|
||||
for _, ext := range extensions {
|
||||
if fileExt == ext {
|
||||
confFiles = append(confFiles, filepath.Join(dir, f.Name()))
|
||||
}
|
||||
}
|
||||
}
|
||||
return confFiles, nil
|
||||
}
|
||||
|
||||
func LoadConf(dir, name string) (*NetworkConfig, error) {
|
||||
files, err := ConfFiles(dir, []string{".conf", ".json"})
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, err
|
||||
case len(files) == 0:
|
||||
return nil, NoConfigsFoundError{Dir: dir}
|
||||
}
|
||||
sort.Strings(files)
|
||||
|
||||
for _, confFile := range files {
|
||||
conf, err := ConfFromFile(confFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if conf.Network.Name == name {
|
||||
return conf, nil
|
||||
}
|
||||
}
|
||||
return nil, NotFoundError{dir, name}
|
||||
}
|
||||
|
||||
func LoadConfList(dir, name string) (*NetworkConfigList, error) {
|
||||
files, err := ConfFiles(dir, []string{".conflist"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Strings(files)
|
||||
|
||||
for _, confFile := range files {
|
||||
conf, err := ConfListFromFile(confFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if conf.Name == name {
|
||||
return conf, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Try and load a network configuration file (instead of list)
|
||||
// from the same name, then upconvert.
|
||||
singleConf, err := LoadConf(dir, name)
|
||||
if err != nil {
|
||||
// A little extra logic so the error makes sense
|
||||
if _, ok := err.(NoConfigsFoundError); len(files) != 0 && ok {
|
||||
// Config lists found but no config files found
|
||||
return nil, NotFoundError{dir, name}
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
return ConfListFromConf(singleConf)
|
||||
}
|
||||
|
||||
func InjectConf(original *NetworkConfig, newValues map[string]interface{}) (*NetworkConfig, error) {
|
||||
config := make(map[string]interface{})
|
||||
err := json.Unmarshal(original.Bytes, &config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unmarshal existing network bytes: %s", err)
|
||||
}
|
||||
|
||||
for key, value := range newValues {
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("keys cannot be empty")
|
||||
}
|
||||
|
||||
if value == nil {
|
||||
return nil, fmt.Errorf("key '%s' value must not be nil", key)
|
||||
}
|
||||
|
||||
config[key] = value
|
||||
}
|
||||
|
||||
newBytes, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ConfFromBytes(newBytes)
|
||||
}
|
||||
|
||||
// ConfListFromConf "upconverts" a network config in to a NetworkConfigList,
|
||||
// with the single network as the only entry in the list.
|
||||
func ConfListFromConf(original *NetworkConfig) (*NetworkConfigList, error) {
|
||||
// Re-deserialize the config's json, then make a raw map configlist.
|
||||
// This may seem a bit strange, but it's to make the Bytes fields
|
||||
// actually make sense. Otherwise, the generated json is littered with
|
||||
// golang default values.
|
||||
|
||||
rawConfig := make(map[string]interface{})
|
||||
if err := json.Unmarshal(original.Bytes, &rawConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawConfigList := map[string]interface{}{
|
||||
"name": original.Network.Name,
|
||||
"cniVersion": original.Network.CNIVersion,
|
||||
"plugins": []interface{}{rawConfig},
|
||||
}
|
||||
|
||||
b, err := json.Marshal(rawConfigList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ConfListFromBytes(b)
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
// Copyright 2015 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package invoke
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type CNIArgs interface {
|
||||
// For use with os/exec; i.e., return nil to inherit the
|
||||
// environment from this process
|
||||
// For use in delegation; inherit the environment from this
|
||||
// process and allow overrides
|
||||
AsEnv() []string
|
||||
}
|
||||
|
||||
type inherited struct{}
|
||||
|
||||
var inheritArgsFromEnv inherited
|
||||
|
||||
func (_ *inherited) AsEnv() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ArgsFromEnv() CNIArgs {
|
||||
return &inheritArgsFromEnv
|
||||
}
|
||||
|
||||
type Args struct {
|
||||
Command string
|
||||
ContainerID string
|
||||
NetNS string
|
||||
PluginArgs [][2]string
|
||||
PluginArgsStr string
|
||||
IfName string
|
||||
Path string
|
||||
}
|
||||
|
||||
// Args implements the CNIArgs interface
|
||||
var _ CNIArgs = &Args{}
|
||||
|
||||
func (args *Args) AsEnv() []string {
|
||||
env := os.Environ()
|
||||
pluginArgsStr := args.PluginArgsStr
|
||||
if pluginArgsStr == "" {
|
||||
pluginArgsStr = stringify(args.PluginArgs)
|
||||
}
|
||||
|
||||
// Duplicated values which come first will be overrided, so we must put the
|
||||
// custom values in the end to avoid being overrided by the process environments.
|
||||
env = append(env,
|
||||
"CNI_COMMAND="+args.Command,
|
||||
"CNI_CONTAINERID="+args.ContainerID,
|
||||
"CNI_NETNS="+args.NetNS,
|
||||
"CNI_ARGS="+pluginArgsStr,
|
||||
"CNI_IFNAME="+args.IfName,
|
||||
"CNI_PATH="+args.Path,
|
||||
)
|
||||
return dedupEnv(env)
|
||||
}
|
||||
|
||||
// taken from rkt/networking/net_plugin.go
|
||||
func stringify(pluginArgs [][2]string) string {
|
||||
entries := make([]string, len(pluginArgs))
|
||||
|
||||
for i, kv := range pluginArgs {
|
||||
entries[i] = strings.Join(kv[:], "=")
|
||||
}
|
||||
|
||||
return strings.Join(entries, ";")
|
||||
}
|
||||
|
||||
// DelegateArgs implements the CNIArgs interface
|
||||
// used for delegation to inherit from environments
|
||||
// and allow some overrides like CNI_COMMAND
|
||||
var _ CNIArgs = &DelegateArgs{}
|
||||
|
||||
type DelegateArgs struct {
|
||||
Command string
|
||||
}
|
||||
|
||||
func (d *DelegateArgs) AsEnv() []string {
|
||||
env := os.Environ()
|
||||
|
||||
// The custom values should come in the end to override the existing
|
||||
// process environment of the same key.
|
||||
env = append(env,
|
||||
"CNI_COMMAND="+d.Command,
|
||||
)
|
||||
return dedupEnv(env)
|
||||
}
|
||||
|
||||
// dedupEnv returns a copy of env with any duplicates removed, in favor of later values.
|
||||
// Items not of the normal environment "key=value" form are preserved unchanged.
|
||||
func dedupEnv(env []string) []string {
|
||||
out := make([]string, 0, len(env))
|
||||
envMap := map[string]string{}
|
||||
|
||||
for _, kv := range env {
|
||||
// find the first "=" in environment, if not, just keep it
|
||||
eq := strings.Index(kv, "=")
|
||||
if eq < 0 {
|
||||
out = append(out, kv)
|
||||
continue
|
||||
}
|
||||
envMap[kv[:eq]] = kv[eq+1:]
|
||||
}
|
||||
|
||||
for k, v := range envMap {
|
||||
out = append(out, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright 2016 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package invoke
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
)
|
||||
|
||||
func delegateCommon(delegatePlugin string, exec Exec) (string, Exec, error) {
|
||||
if exec == nil {
|
||||
exec = defaultExec
|
||||
}
|
||||
|
||||
paths := filepath.SplitList(os.Getenv("CNI_PATH"))
|
||||
pluginPath, err := exec.FindInPath(delegatePlugin, paths)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return pluginPath, exec, nil
|
||||
}
|
||||
|
||||
// DelegateAdd calls the given delegate plugin with the CNI ADD action and
|
||||
// JSON configuration
|
||||
func DelegateAdd(ctx context.Context, delegatePlugin string, netconf []byte, exec Exec) (types.Result, error) {
|
||||
pluginPath, realExec, err := delegateCommon(delegatePlugin, exec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// DelegateAdd will override the original "CNI_COMMAND" env from process with ADD
|
||||
return ExecPluginWithResult(ctx, pluginPath, netconf, delegateArgs("ADD"), realExec)
|
||||
}
|
||||
|
||||
// DelegateCheck calls the given delegate plugin with the CNI CHECK action and
|
||||
// JSON configuration
|
||||
func DelegateCheck(ctx context.Context, delegatePlugin string, netconf []byte, exec Exec) error {
|
||||
pluginPath, realExec, err := delegateCommon(delegatePlugin, exec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// DelegateCheck will override the original CNI_COMMAND env from process with CHECK
|
||||
return ExecPluginWithoutResult(ctx, pluginPath, netconf, delegateArgs("CHECK"), realExec)
|
||||
}
|
||||
|
||||
// DelegateDel calls the given delegate plugin with the CNI DEL action and
|
||||
// JSON configuration
|
||||
func DelegateDel(ctx context.Context, delegatePlugin string, netconf []byte, exec Exec) error {
|
||||
pluginPath, realExec, err := delegateCommon(delegatePlugin, exec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// DelegateDel will override the original CNI_COMMAND env from process with DEL
|
||||
return ExecPluginWithoutResult(ctx, pluginPath, netconf, delegateArgs("DEL"), realExec)
|
||||
}
|
||||
|
||||
// return CNIArgs used by delegation
|
||||
func delegateArgs(action string) *DelegateArgs {
|
||||
return &DelegateArgs{
|
||||
Command: action,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
// Copyright 2015 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package invoke
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
)
|
||||
|
||||
// Exec is an interface encapsulates all operations that deal with finding
|
||||
// and executing a CNI plugin. Tests may provide a fake implementation
|
||||
// to avoid writing fake plugins to temporary directories during the test.
|
||||
type Exec interface {
|
||||
ExecPlugin(ctx context.Context, pluginPath string, stdinData []byte, environ []string) ([]byte, error)
|
||||
FindInPath(plugin string, paths []string) (string, error)
|
||||
Decode(jsonBytes []byte) (version.PluginInfo, error)
|
||||
}
|
||||
|
||||
// For example, a testcase could pass an instance of the following fakeExec
|
||||
// object to ExecPluginWithResult() to verify the incoming stdin and environment
|
||||
// and provide a tailored response:
|
||||
//
|
||||
//import (
|
||||
// "encoding/json"
|
||||
// "path"
|
||||
// "strings"
|
||||
//)
|
||||
//
|
||||
//type fakeExec struct {
|
||||
// version.PluginDecoder
|
||||
//}
|
||||
//
|
||||
//func (f *fakeExec) ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) {
|
||||
// net := &types.NetConf{}
|
||||
// err := json.Unmarshal(stdinData, net)
|
||||
// if err != nil {
|
||||
// return nil, fmt.Errorf("failed to unmarshal configuration: %v", err)
|
||||
// }
|
||||
// pluginName := path.Base(pluginPath)
|
||||
// if pluginName != net.Type {
|
||||
// return nil, fmt.Errorf("plugin name %q did not match config type %q", pluginName, net.Type)
|
||||
// }
|
||||
// for _, e := range environ {
|
||||
// // Check environment for forced failure request
|
||||
// parts := strings.Split(e, "=")
|
||||
// if len(parts) > 0 && parts[0] == "FAIL" {
|
||||
// return nil, fmt.Errorf("failed to execute plugin %s", pluginName)
|
||||
// }
|
||||
// }
|
||||
// return []byte("{\"CNIVersion\":\"0.4.0\"}"), nil
|
||||
//}
|
||||
//
|
||||
//func (f *fakeExec) FindInPath(plugin string, paths []string) (string, error) {
|
||||
// if len(paths) > 0 {
|
||||
// return path.Join(paths[0], plugin), nil
|
||||
// }
|
||||
// return "", fmt.Errorf("failed to find plugin %s in paths %v", plugin, paths)
|
||||
//}
|
||||
|
||||
func ExecPluginWithResult(ctx context.Context, pluginPath string, netconf []byte, args CNIArgs, exec Exec) (types.Result, error) {
|
||||
if exec == nil {
|
||||
exec = defaultExec
|
||||
}
|
||||
|
||||
stdoutBytes, err := exec.ExecPlugin(ctx, pluginPath, netconf, args.AsEnv())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Plugin must return result in same version as specified in netconf
|
||||
versionDecoder := &version.ConfigDecoder{}
|
||||
confVersion, err := versionDecoder.Decode(netconf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return version.NewResult(confVersion, stdoutBytes)
|
||||
}
|
||||
|
||||
func ExecPluginWithoutResult(ctx context.Context, pluginPath string, netconf []byte, args CNIArgs, exec Exec) error {
|
||||
if exec == nil {
|
||||
exec = defaultExec
|
||||
}
|
||||
_, err := exec.ExecPlugin(ctx, pluginPath, netconf, args.AsEnv())
|
||||
return err
|
||||
}
|
||||
|
||||
// GetVersionInfo returns the version information available about the plugin.
|
||||
// For recent-enough plugins, it uses the information returned by the VERSION
|
||||
// command. For older plugins which do not recognize that command, it reports
|
||||
// version 0.1.0
|
||||
func GetVersionInfo(ctx context.Context, pluginPath string, exec Exec) (version.PluginInfo, error) {
|
||||
if exec == nil {
|
||||
exec = defaultExec
|
||||
}
|
||||
args := &Args{
|
||||
Command: "VERSION",
|
||||
|
||||
// set fake values required by plugins built against an older version of skel
|
||||
NetNS: "dummy",
|
||||
IfName: "dummy",
|
||||
Path: "dummy",
|
||||
}
|
||||
stdin := []byte(fmt.Sprintf(`{"cniVersion":%q}`, version.Current()))
|
||||
stdoutBytes, err := exec.ExecPlugin(ctx, pluginPath, stdin, args.AsEnv())
|
||||
if err != nil {
|
||||
if err.Error() == "unknown CNI_COMMAND: VERSION" {
|
||||
return version.PluginSupports("0.1.0"), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return exec.Decode(stdoutBytes)
|
||||
}
|
||||
|
||||
// DefaultExec is an object that implements the Exec interface which looks
|
||||
// for and executes plugins from disk.
|
||||
type DefaultExec struct {
|
||||
*RawExec
|
||||
version.PluginDecoder
|
||||
}
|
||||
|
||||
// DefaultExec implements the Exec interface
|
||||
var _ Exec = &DefaultExec{}
|
||||
|
||||
var defaultExec = &DefaultExec{
|
||||
RawExec: &RawExec{Stderr: os.Stderr},
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright 2015 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package invoke
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// FindInPath returns the full path of the plugin by searching in the provided path
|
||||
func FindInPath(plugin string, paths []string) (string, error) {
|
||||
if plugin == "" {
|
||||
return "", fmt.Errorf("no plugin name provided")
|
||||
}
|
||||
|
||||
if len(paths) == 0 {
|
||||
return "", fmt.Errorf("no paths provided")
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
for _, fe := range ExecutableFileExtensions {
|
||||
fullpath := filepath.Join(path, plugin) + fe
|
||||
if fi, err := os.Stat(fullpath); err == nil && fi.Mode().IsRegular() {
|
||||
return fullpath, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("failed to find plugin %q in path %s", plugin, paths)
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright 2016 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
|
||||
package invoke
|
||||
|
||||
// Valid file extensions for plugin executables.
|
||||
var ExecutableFileExtensions = []string{""}
|
18
vendor/github.com/containernetworking/cni/pkg/invoke/os_windows.go
generated
vendored
Normal file
18
vendor/github.com/containernetworking/cni/pkg/invoke/os_windows.go
generated
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
// Copyright 2016 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package invoke
|
||||
|
||||
// Valid file extensions for plugin executables.
|
||||
var ExecutableFileExtensions = []string{".exe", ""}
|
|
@ -0,0 +1,62 @@
|
|||
// Copyright 2016 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package invoke
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
)
|
||||
|
||||
type RawExec struct {
|
||||
Stderr io.Writer
|
||||
}
|
||||
|
||||
func (e *RawExec) ExecPlugin(ctx context.Context, pluginPath string, stdinData []byte, environ []string) ([]byte, error) {
|
||||
stdout := &bytes.Buffer{}
|
||||
c := exec.CommandContext(ctx, pluginPath)
|
||||
c.Env = environ
|
||||
c.Stdin = bytes.NewBuffer(stdinData)
|
||||
c.Stdout = stdout
|
||||
c.Stderr = e.Stderr
|
||||
if err := c.Run(); err != nil {
|
||||
return nil, pluginErr(err, stdout.Bytes())
|
||||
}
|
||||
|
||||
return stdout.Bytes(), nil
|
||||
}
|
||||
|
||||
func pluginErr(err error, output []byte) error {
|
||||
if _, ok := err.(*exec.ExitError); ok {
|
||||
emsg := types.Error{}
|
||||
if len(output) == 0 {
|
||||
emsg.Msg = "netplugin failed with no error message"
|
||||
} else if perr := json.Unmarshal(output, &emsg); perr != nil {
|
||||
emsg.Msg = fmt.Sprintf("netplugin failed but error parsing its diagnostic message %q: %v", string(output), perr)
|
||||
}
|
||||
return &emsg
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *RawExec) FindInPath(plugin string, paths []string) (string, error) {
|
||||
return FindInPath(plugin, paths)
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
// Copyright 2016 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package types020
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
)
|
||||
|
||||
const ImplementedSpecVersion string = "0.2.0"
|
||||
|
||||
var SupportedVersions = []string{"", "0.1.0", ImplementedSpecVersion}
|
||||
|
||||
// Compatibility types for CNI version 0.1.0 and 0.2.0
|
||||
|
||||
func NewResult(data []byte) (types.Result, error) {
|
||||
result := &Result{}
|
||||
if err := json.Unmarshal(data, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func GetResult(r types.Result) (*Result, error) {
|
||||
// We expect version 0.1.0/0.2.0 results
|
||||
result020, err := r.GetAsVersion(ImplementedSpecVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, ok := result020.(*Result)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert result")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Result is what gets returned from the plugin (via stdout) to the caller
|
||||
type Result struct {
|
||||
CNIVersion string `json:"cniVersion,omitempty"`
|
||||
IP4 *IPConfig `json:"ip4,omitempty"`
|
||||
IP6 *IPConfig `json:"ip6,omitempty"`
|
||||
DNS types.DNS `json:"dns,omitempty"`
|
||||
}
|
||||
|
||||
func (r *Result) Version() string {
|
||||
return ImplementedSpecVersion
|
||||
}
|
||||
|
||||
func (r *Result) GetAsVersion(version string) (types.Result, error) {
|
||||
for _, supportedVersion := range SupportedVersions {
|
||||
if version == supportedVersion {
|
||||
r.CNIVersion = version
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("cannot convert version %q to %s", SupportedVersions, version)
|
||||
}
|
||||
|
||||
func (r *Result) Print() error {
|
||||
return r.PrintTo(os.Stdout)
|
||||
}
|
||||
|
||||
func (r *Result) PrintTo(writer io.Writer) error {
|
||||
data, err := json.MarshalIndent(r, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = writer.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
// String returns a formatted string in the form of "[IP4: $1,][ IP6: $2,] DNS: $3" where
|
||||
// $1 represents the receiver's IPv4, $2 represents the receiver's IPv6 and $3 the
|
||||
// receiver's DNS. If $1 or $2 are nil, they won't be present in the returned string.
|
||||
func (r *Result) String() string {
|
||||
var str string
|
||||
if r.IP4 != nil {
|
||||
str = fmt.Sprintf("IP4:%+v, ", *r.IP4)
|
||||
}
|
||||
if r.IP6 != nil {
|
||||
str += fmt.Sprintf("IP6:%+v, ", *r.IP6)
|
||||
}
|
||||
return fmt.Sprintf("%sDNS:%+v", str, r.DNS)
|
||||
}
|
||||
|
||||
// IPConfig contains values necessary to configure an interface
|
||||
type IPConfig struct {
|
||||
IP net.IPNet
|
||||
Gateway net.IP
|
||||
Routes []types.Route
|
||||
}
|
||||
|
||||
// net.IPNet is not JSON (un)marshallable so this duality is needed
|
||||
// for our custom IPNet type
|
||||
|
||||
// JSON (un)marshallable types
|
||||
type ipConfig struct {
|
||||
IP types.IPNet `json:"ip"`
|
||||
Gateway net.IP `json:"gateway,omitempty"`
|
||||
Routes []types.Route `json:"routes,omitempty"`
|
||||
}
|
||||
|
||||
func (c *IPConfig) MarshalJSON() ([]byte, error) {
|
||||
ipc := ipConfig{
|
||||
IP: types.IPNet(c.IP),
|
||||
Gateway: c.Gateway,
|
||||
Routes: c.Routes,
|
||||
}
|
||||
|
||||
return json.Marshal(ipc)
|
||||
}
|
||||
|
||||
func (c *IPConfig) UnmarshalJSON(data []byte) error {
|
||||
ipc := ipConfig{}
|
||||
if err := json.Unmarshal(data, &ipc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.IP = net.IPNet(ipc.IP)
|
||||
c.Gateway = ipc.Gateway
|
||||
c.Routes = ipc.Routes
|
||||
return nil
|
||||
}
|
|
@ -41,6 +41,16 @@ func (b *UnmarshallableBool) UnmarshalText(data []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// UnmarshallableString typedef for builtin string
|
||||
type UnmarshallableString string
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaler interface.
|
||||
// Returns the string
|
||||
func (s *UnmarshallableString) UnmarshalText(data []byte) error {
|
||||
*s = UnmarshallableString(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommonArgs contains the IgnoreUnknown argument
|
||||
// and must be embedded by all Arg structs
|
||||
type CommonArgs struct {
|
||||
|
@ -53,6 +63,12 @@ func GetKeyField(keyString string, v reflect.Value) reflect.Value {
|
|||
return v.Elem().FieldByName(keyString)
|
||||
}
|
||||
|
||||
// UnmarshalableArgsError is used to indicate error unmarshalling args
|
||||
// from the args-string in the form "K=V;K2=V2;..."
|
||||
type UnmarshalableArgsError struct {
|
||||
error
|
||||
}
|
||||
|
||||
// LoadArgs parses args from a string in the form "K=V;K2=V2;..."
|
||||
func LoadArgs(args string, container interface{}) error {
|
||||
if args == "" {
|
||||
|
@ -75,8 +91,13 @@ func LoadArgs(args string, container interface{}) error {
|
|||
unknownArgs = append(unknownArgs, pair)
|
||||
continue
|
||||
}
|
||||
|
||||
u := keyField.Addr().Interface().(encoding.TextUnmarshaler)
|
||||
keyFieldIface := keyField.Addr().Interface()
|
||||
u, ok := keyFieldIface.(encoding.TextUnmarshaler)
|
||||
if !ok {
|
||||
return UnmarshalableArgsError{fmt.Errorf(
|
||||
"ARGS: cannot unmarshal into field '%s' - type '%s' does not implement encoding.TextUnmarshaler",
|
||||
keyString, reflect.TypeOf(keyFieldIface))}
|
||||
}
|
||||
err := u.UnmarshalText([]byte(valueString))
|
||||
if err != nil {
|
||||
return fmt.Errorf("ARGS: error parsing value of pair %q: %v)", pair, err)
|
||||
|
|
293
vendor/github.com/containernetworking/cni/pkg/types/current/types.go
generated
vendored
Normal file
293
vendor/github.com/containernetworking/cni/pkg/types/current/types.go
generated
vendored
Normal file
|
@ -0,0 +1,293 @@
|
|||
// Copyright 2016 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package current
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/020"
|
||||
)
|
||||
|
||||
const ImplementedSpecVersion string = "0.4.0"
|
||||
|
||||
var SupportedVersions = []string{"0.3.0", "0.3.1", ImplementedSpecVersion}
|
||||
|
||||
func NewResult(data []byte) (types.Result, error) {
|
||||
result := &Result{}
|
||||
if err := json.Unmarshal(data, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func GetResult(r types.Result) (*Result, error) {
|
||||
resultCurrent, err := r.GetAsVersion(ImplementedSpecVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, ok := resultCurrent.(*Result)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert result")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var resultConverters = []struct {
|
||||
versions []string
|
||||
convert func(types.Result) (*Result, error)
|
||||
}{
|
||||
{types020.SupportedVersions, convertFrom020},
|
||||
{SupportedVersions, convertFrom030},
|
||||
}
|
||||
|
||||
func convertFrom020(result types.Result) (*Result, error) {
|
||||
oldResult, err := types020.GetResult(result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newResult := &Result{
|
||||
CNIVersion: ImplementedSpecVersion,
|
||||
DNS: oldResult.DNS,
|
||||
Routes: []*types.Route{},
|
||||
}
|
||||
|
||||
if oldResult.IP4 != nil {
|
||||
newResult.IPs = append(newResult.IPs, &IPConfig{
|
||||
Version: "4",
|
||||
Address: oldResult.IP4.IP,
|
||||
Gateway: oldResult.IP4.Gateway,
|
||||
})
|
||||
for _, route := range oldResult.IP4.Routes {
|
||||
newResult.Routes = append(newResult.Routes, &types.Route{
|
||||
Dst: route.Dst,
|
||||
GW: route.GW,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if oldResult.IP6 != nil {
|
||||
newResult.IPs = append(newResult.IPs, &IPConfig{
|
||||
Version: "6",
|
||||
Address: oldResult.IP6.IP,
|
||||
Gateway: oldResult.IP6.Gateway,
|
||||
})
|
||||
for _, route := range oldResult.IP6.Routes {
|
||||
newResult.Routes = append(newResult.Routes, &types.Route{
|
||||
Dst: route.Dst,
|
||||
GW: route.GW,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return newResult, nil
|
||||
}
|
||||
|
||||
func convertFrom030(result types.Result) (*Result, error) {
|
||||
newResult, ok := result.(*Result)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert result")
|
||||
}
|
||||
newResult.CNIVersion = ImplementedSpecVersion
|
||||
return newResult, nil
|
||||
}
|
||||
|
||||
func NewResultFromResult(result types.Result) (*Result, error) {
|
||||
version := result.Version()
|
||||
for _, converter := range resultConverters {
|
||||
for _, supportedVersion := range converter.versions {
|
||||
if version == supportedVersion {
|
||||
return converter.convert(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported CNI result22 version %q", version)
|
||||
}
|
||||
|
||||
// Result is what gets returned from the plugin (via stdout) to the caller
|
||||
type Result struct {
|
||||
CNIVersion string `json:"cniVersion,omitempty"`
|
||||
Interfaces []*Interface `json:"interfaces,omitempty"`
|
||||
IPs []*IPConfig `json:"ips,omitempty"`
|
||||
Routes []*types.Route `json:"routes,omitempty"`
|
||||
DNS types.DNS `json:"dns,omitempty"`
|
||||
}
|
||||
|
||||
// Convert to the older 0.2.0 CNI spec Result type
|
||||
func (r *Result) convertTo020() (*types020.Result, error) {
|
||||
oldResult := &types020.Result{
|
||||
CNIVersion: types020.ImplementedSpecVersion,
|
||||
DNS: r.DNS,
|
||||
}
|
||||
|
||||
for _, ip := range r.IPs {
|
||||
// Only convert the first IP address of each version as 0.2.0
|
||||
// and earlier cannot handle multiple IP addresses
|
||||
if ip.Version == "4" && oldResult.IP4 == nil {
|
||||
oldResult.IP4 = &types020.IPConfig{
|
||||
IP: ip.Address,
|
||||
Gateway: ip.Gateway,
|
||||
}
|
||||
} else if ip.Version == "6" && oldResult.IP6 == nil {
|
||||
oldResult.IP6 = &types020.IPConfig{
|
||||
IP: ip.Address,
|
||||
Gateway: ip.Gateway,
|
||||
}
|
||||
}
|
||||
|
||||
if oldResult.IP4 != nil && oldResult.IP6 != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, route := range r.Routes {
|
||||
is4 := route.Dst.IP.To4() != nil
|
||||
if is4 && oldResult.IP4 != nil {
|
||||
oldResult.IP4.Routes = append(oldResult.IP4.Routes, types.Route{
|
||||
Dst: route.Dst,
|
||||
GW: route.GW,
|
||||
})
|
||||
} else if !is4 && oldResult.IP6 != nil {
|
||||
oldResult.IP6.Routes = append(oldResult.IP6.Routes, types.Route{
|
||||
Dst: route.Dst,
|
||||
GW: route.GW,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if oldResult.IP4 == nil && oldResult.IP6 == nil {
|
||||
return nil, fmt.Errorf("cannot convert: no valid IP addresses")
|
||||
}
|
||||
|
||||
return oldResult, nil
|
||||
}
|
||||
|
||||
func (r *Result) Version() string {
|
||||
return ImplementedSpecVersion
|
||||
}
|
||||
|
||||
func (r *Result) GetAsVersion(version string) (types.Result, error) {
|
||||
switch version {
|
||||
case "0.3.0", "0.3.1", ImplementedSpecVersion:
|
||||
r.CNIVersion = version
|
||||
return r, nil
|
||||
case types020.SupportedVersions[0], types020.SupportedVersions[1], types020.SupportedVersions[2]:
|
||||
return r.convertTo020()
|
||||
}
|
||||
return nil, fmt.Errorf("cannot convert version 0.3.x to %q", version)
|
||||
}
|
||||
|
||||
func (r *Result) Print() error {
|
||||
return r.PrintTo(os.Stdout)
|
||||
}
|
||||
|
||||
func (r *Result) PrintTo(writer io.Writer) error {
|
||||
data, err := json.MarshalIndent(r, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = writer.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
// String returns a formatted string in the form of "[Interfaces: $1,][ IP: $2,] DNS: $3" where
|
||||
// $1 represents the receiver's Interfaces, $2 represents the receiver's IP addresses and $3 the
|
||||
// receiver's DNS. If $1 or $2 are nil, they won't be present in the returned string.
|
||||
func (r *Result) String() string {
|
||||
var str string
|
||||
if len(r.Interfaces) > 0 {
|
||||
str += fmt.Sprintf("Interfaces:%+v, ", r.Interfaces)
|
||||
}
|
||||
if len(r.IPs) > 0 {
|
||||
str += fmt.Sprintf("IP:%+v, ", r.IPs)
|
||||
}
|
||||
if len(r.Routes) > 0 {
|
||||
str += fmt.Sprintf("Routes:%+v, ", r.Routes)
|
||||
}
|
||||
return fmt.Sprintf("%sDNS:%+v", str, r.DNS)
|
||||
}
|
||||
|
||||
// Convert this old version result to the current CNI version result
|
||||
func (r *Result) Convert() (*Result, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Interface contains values about the created interfaces
|
||||
type Interface struct {
|
||||
Name string `json:"name"`
|
||||
Mac string `json:"mac,omitempty"`
|
||||
Sandbox string `json:"sandbox,omitempty"`
|
||||
}
|
||||
|
||||
func (i *Interface) String() string {
|
||||
return fmt.Sprintf("%+v", *i)
|
||||
}
|
||||
|
||||
// Int returns a pointer to the int value passed in. Used to
|
||||
// set the IPConfig.Interface field.
|
||||
func Int(v int) *int {
|
||||
return &v
|
||||
}
|
||||
|
||||
// IPConfig contains values necessary to configure an IP address on an interface
|
||||
type IPConfig struct {
|
||||
// IP version, either "4" or "6"
|
||||
Version string
|
||||
// Index into Result structs Interfaces list
|
||||
Interface *int
|
||||
Address net.IPNet
|
||||
Gateway net.IP
|
||||
}
|
||||
|
||||
func (i *IPConfig) String() string {
|
||||
return fmt.Sprintf("%+v", *i)
|
||||
}
|
||||
|
||||
// JSON (un)marshallable types
|
||||
type ipConfig struct {
|
||||
Version string `json:"version"`
|
||||
Interface *int `json:"interface,omitempty"`
|
||||
Address types.IPNet `json:"address"`
|
||||
Gateway net.IP `json:"gateway,omitempty"`
|
||||
}
|
||||
|
||||
func (c *IPConfig) MarshalJSON() ([]byte, error) {
|
||||
ipc := ipConfig{
|
||||
Version: c.Version,
|
||||
Interface: c.Interface,
|
||||
Address: types.IPNet(c.Address),
|
||||
Gateway: c.Gateway,
|
||||
}
|
||||
|
||||
return json.Marshal(ipc)
|
||||
}
|
||||
|
||||
func (c *IPConfig) UnmarshalJSON(data []byte) error {
|
||||
ipc := ipConfig{}
|
||||
if err := json.Unmarshal(data, &ipc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Version = ipc.Version
|
||||
c.Interface = ipc.Interface
|
||||
c.Address = net.IPNet(ipc.Address)
|
||||
c.Gateway = ipc.Gateway
|
||||
return nil
|
||||
}
|
|
@ -16,7 +16,9 @@ package types
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
)
|
||||
|
@ -57,44 +59,59 @@ func (n *IPNet) UnmarshalJSON(data []byte) error {
|
|||
|
||||
// NetConf describes a network.
|
||||
type NetConf struct {
|
||||
CNIVersion string `json:"cniVersion,omitempty"`
|
||||
|
||||
Name string `json:"name,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
IPAM struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
} `json:"ipam,omitempty"`
|
||||
Capabilities map[string]bool `json:"capabilities,omitempty"`
|
||||
IPAM IPAM `json:"ipam,omitempty"`
|
||||
DNS DNS `json:"dns"`
|
||||
|
||||
RawPrevResult map[string]interface{} `json:"prevResult,omitempty"`
|
||||
PrevResult Result `json:"-"`
|
||||
}
|
||||
|
||||
// Result is what gets returned from the plugin (via stdout) to the caller
|
||||
type Result struct {
|
||||
IP4 *IPConfig `json:"ip4,omitempty"`
|
||||
IP6 *IPConfig `json:"ip6,omitempty"`
|
||||
DNS DNS `json:"dns,omitempty"`
|
||||
type IPAM struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
func (r *Result) Print() error {
|
||||
return prettyPrint(r)
|
||||
// NetConfList describes an ordered list of networks.
|
||||
type NetConfList struct {
|
||||
CNIVersion string `json:"cniVersion,omitempty"`
|
||||
|
||||
Name string `json:"name,omitempty"`
|
||||
DisableCheck bool `json:"disableCheck,omitempty"`
|
||||
Plugins []*NetConf `json:"plugins,omitempty"`
|
||||
}
|
||||
|
||||
// String returns a formatted string in the form of "[IP4: $1,][ IP6: $2,] DNS: $3" where
|
||||
// $1 represents the receiver's IPv4, $2 represents the receiver's IPv6 and $3 the
|
||||
// receiver's DNS. If $1 or $2 are nil, they won't be present in the returned string.
|
||||
func (r *Result) String() string {
|
||||
var str string
|
||||
if r.IP4 != nil {
|
||||
str = fmt.Sprintf("IP4:%+v, ", *r.IP4)
|
||||
type ResultFactoryFunc func([]byte) (Result, error)
|
||||
|
||||
// Result is an interface that provides the result of plugin execution
|
||||
type Result interface {
|
||||
// The highest CNI specification result version the result supports
|
||||
// without having to convert
|
||||
Version() string
|
||||
|
||||
// Returns the result converted into the requested CNI specification
|
||||
// result version, or an error if conversion failed
|
||||
GetAsVersion(version string) (Result, error)
|
||||
|
||||
// Prints the result in JSON format to stdout
|
||||
Print() error
|
||||
|
||||
// Prints the result in JSON format to provided writer
|
||||
PrintTo(writer io.Writer) error
|
||||
|
||||
// Returns a JSON string representation of the result
|
||||
String() string
|
||||
}
|
||||
|
||||
func PrintResult(result Result, version string) error {
|
||||
newResult, err := result.GetAsVersion(version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.IP6 != nil {
|
||||
str += fmt.Sprintf("IP6:%+v, ", *r.IP6)
|
||||
}
|
||||
return fmt.Sprintf("%sDNS:%+v", str, r.DNS)
|
||||
}
|
||||
|
||||
// IPConfig contains values necessary to configure an interface
|
||||
type IPConfig struct {
|
||||
IP net.IPNet
|
||||
Gateway net.IP
|
||||
Routes []Route
|
||||
return newResult.Print()
|
||||
}
|
||||
|
||||
// DNS contains values interesting for DNS resolvers
|
||||
|
@ -110,6 +127,18 @@ type Route struct {
|
|||
GW net.IP
|
||||
}
|
||||
|
||||
func (r *Route) String() string {
|
||||
return fmt.Sprintf("%+v", *r)
|
||||
}
|
||||
|
||||
// Well known error codes
|
||||
// see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes
|
||||
const (
|
||||
ErrUnknown uint = iota // 0
|
||||
ErrIncompatibleCNIVersion // 1
|
||||
ErrUnsupportedField // 2
|
||||
)
|
||||
|
||||
type Error struct {
|
||||
Code uint `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
|
@ -117,7 +146,11 @@ type Error struct {
|
|||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return e.Msg
|
||||
details := ""
|
||||
if e.Details != "" {
|
||||
details = fmt.Sprintf("; %v", e.Details)
|
||||
}
|
||||
return fmt.Sprintf("%v%v", e.Msg, details)
|
||||
}
|
||||
|
||||
func (e *Error) Print() error {
|
||||
|
@ -128,39 +161,11 @@ func (e *Error) Print() error {
|
|||
// for our custom IPNet type
|
||||
|
||||
// JSON (un)marshallable types
|
||||
type ipConfig struct {
|
||||
IP IPNet `json:"ip"`
|
||||
Gateway net.IP `json:"gateway,omitempty"`
|
||||
Routes []Route `json:"routes,omitempty"`
|
||||
}
|
||||
|
||||
type route struct {
|
||||
Dst IPNet `json:"dst"`
|
||||
GW net.IP `json:"gw,omitempty"`
|
||||
}
|
||||
|
||||
func (c *IPConfig) MarshalJSON() ([]byte, error) {
|
||||
ipc := ipConfig{
|
||||
IP: IPNet(c.IP),
|
||||
Gateway: c.Gateway,
|
||||
Routes: c.Routes,
|
||||
}
|
||||
|
||||
return json.Marshal(ipc)
|
||||
}
|
||||
|
||||
func (c *IPConfig) UnmarshalJSON(data []byte) error {
|
||||
ipc := ipConfig{}
|
||||
if err := json.Unmarshal(data, &ipc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.IP = net.IPNet(ipc.IP)
|
||||
c.Gateway = ipc.Gateway
|
||||
c.Routes = ipc.Routes
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Route) UnmarshalJSON(data []byte) error {
|
||||
rt := route{}
|
||||
if err := json.Unmarshal(data, &rt); err != nil {
|
||||
|
@ -172,7 +177,7 @@ func (r *Route) UnmarshalJSON(data []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (r *Route) MarshalJSON() ([]byte, error) {
|
||||
func (r Route) MarshalJSON() ([]byte, error) {
|
||||
rt := route{
|
||||
Dst: IPNet(r.Dst),
|
||||
GW: r.GW,
|
||||
|
@ -189,3 +194,6 @@ func prettyPrint(obj interface{}) error {
|
|||
_, err = os.Stdout.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
// NotImplementedError is used to indicate that a method is not implemented for the given platform
|
||||
var NotImplementedError = errors.New("Not Implemented")
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright 2016 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package version
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ConfigDecoder can decode the CNI version available in network config data
|
||||
type ConfigDecoder struct{}
|
||||
|
||||
func (*ConfigDecoder) Decode(jsonBytes []byte) (string, error) {
|
||||
var conf struct {
|
||||
CNIVersion string `json:"cniVersion"`
|
||||
}
|
||||
err := json.Unmarshal(jsonBytes, &conf)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decoding version from network config: %s", err)
|
||||
}
|
||||
if conf.CNIVersion == "" {
|
||||
return "0.1.0", nil
|
||||
}
|
||||
return conf.CNIVersion, nil
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
// Copyright 2016 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package version
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PluginInfo reports information about CNI versioning
|
||||
type PluginInfo interface {
|
||||
// SupportedVersions returns one or more CNI spec versions that the plugin
|
||||
// supports. If input is provided in one of these versions, then the plugin
|
||||
// promises to use the same CNI version in its response
|
||||
SupportedVersions() []string
|
||||
|
||||
// Encode writes this CNI version information as JSON to the given Writer
|
||||
Encode(io.Writer) error
|
||||
}
|
||||
|
||||
type pluginInfo struct {
|
||||
CNIVersion_ string `json:"cniVersion"`
|
||||
SupportedVersions_ []string `json:"supportedVersions,omitempty"`
|
||||
}
|
||||
|
||||
// pluginInfo implements the PluginInfo interface
|
||||
var _ PluginInfo = &pluginInfo{}
|
||||
|
||||
func (p *pluginInfo) Encode(w io.Writer) error {
|
||||
return json.NewEncoder(w).Encode(p)
|
||||
}
|
||||
|
||||
func (p *pluginInfo) SupportedVersions() []string {
|
||||
return p.SupportedVersions_
|
||||
}
|
||||
|
||||
// PluginSupports returns a new PluginInfo that will report the given versions
|
||||
// as supported
|
||||
func PluginSupports(supportedVersions ...string) PluginInfo {
|
||||
if len(supportedVersions) < 1 {
|
||||
panic("programmer error: you must support at least one version")
|
||||
}
|
||||
return &pluginInfo{
|
||||
CNIVersion_: Current(),
|
||||
SupportedVersions_: supportedVersions,
|
||||
}
|
||||
}
|
||||
|
||||
// PluginDecoder can decode the response returned by a plugin's VERSION command
|
||||
type PluginDecoder struct{}
|
||||
|
||||
func (*PluginDecoder) Decode(jsonBytes []byte) (PluginInfo, error) {
|
||||
var info pluginInfo
|
||||
err := json.Unmarshal(jsonBytes, &info)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding version info: %s", err)
|
||||
}
|
||||
if info.CNIVersion_ == "" {
|
||||
return nil, fmt.Errorf("decoding version info: missing field cniVersion")
|
||||
}
|
||||
if len(info.SupportedVersions_) == 0 {
|
||||
if info.CNIVersion_ == "0.2.0" {
|
||||
return PluginSupports("0.1.0", "0.2.0"), nil
|
||||
}
|
||||
return nil, fmt.Errorf("decoding version info: missing field supportedVersions")
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
// ParseVersion parses a version string like "3.0.1" or "0.4.5" into major,
|
||||
// minor, and micro numbers or returns an error
|
||||
func ParseVersion(version string) (int, int, int, error) {
|
||||
var major, minor, micro int
|
||||
if version == "" {
|
||||
return -1, -1, -1, fmt.Errorf("invalid version %q: the version is empty", version)
|
||||
}
|
||||
|
||||
parts := strings.Split(version, ".")
|
||||
if len(parts) >= 4 {
|
||||
return -1, -1, -1, fmt.Errorf("invalid version %q: too many parts", version)
|
||||
}
|
||||
|
||||
major, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return -1, -1, -1, fmt.Errorf("failed to convert major version part %q: %v", parts[0], err)
|
||||
}
|
||||
|
||||
if len(parts) >= 2 {
|
||||
minor, err = strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return -1, -1, -1, fmt.Errorf("failed to convert minor version part %q: %v", parts[1], err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(parts) >= 3 {
|
||||
micro, err = strconv.Atoi(parts[2])
|
||||
if err != nil {
|
||||
return -1, -1, -1, fmt.Errorf("failed to convert micro version part %q: %v", parts[2], err)
|
||||
}
|
||||
}
|
||||
|
||||
return major, minor, micro, nil
|
||||
}
|
||||
|
||||
// GreaterThanOrEqualTo takes two string versions, parses them into major/minor/micro
|
||||
// numbers, and compares them to determine whether the first version is greater
|
||||
// than or equal to the second
|
||||
func GreaterThanOrEqualTo(version, otherVersion string) (bool, error) {
|
||||
firstMajor, firstMinor, firstMicro, err := ParseVersion(version)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
secondMajor, secondMinor, secondMicro, err := ParseVersion(otherVersion)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if firstMajor > secondMajor {
|
||||
return true, nil
|
||||
} else if firstMajor == secondMajor {
|
||||
if firstMinor > secondMinor {
|
||||
return true, nil
|
||||
} else if firstMinor == secondMinor && firstMicro >= secondMicro {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
49
vendor/github.com/containernetworking/cni/pkg/version/reconcile.go
generated
vendored
Normal file
49
vendor/github.com/containernetworking/cni/pkg/version/reconcile.go
generated
vendored
Normal file
|
@ -0,0 +1,49 @@
|
|||
// Copyright 2016 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package version
|
||||
|
||||
import "fmt"
|
||||
|
||||
type ErrorIncompatible struct {
|
||||
Config string
|
||||
Supported []string
|
||||
}
|
||||
|
||||
func (e *ErrorIncompatible) Details() string {
|
||||
return fmt.Sprintf("config is %q, plugin supports %q", e.Config, e.Supported)
|
||||
}
|
||||
|
||||
func (e *ErrorIncompatible) Error() string {
|
||||
return fmt.Sprintf("incompatible CNI versions: %s", e.Details())
|
||||
}
|
||||
|
||||
type Reconciler struct{}
|
||||
|
||||
func (r *Reconciler) Check(configVersion string, pluginInfo PluginInfo) *ErrorIncompatible {
|
||||
return r.CheckRaw(configVersion, pluginInfo.SupportedVersions())
|
||||
}
|
||||
|
||||
func (*Reconciler) CheckRaw(configVersion string, supportedVersions []string) *ErrorIncompatible {
|
||||
for _, supportedVersion := range supportedVersions {
|
||||
if configVersion == supportedVersion {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return &ErrorIncompatible{
|
||||
Config: configVersion,
|
||||
Supported: supportedVersions,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
// Copyright 2016 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package version
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/020"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
)
|
||||
|
||||
// Current reports the version of the CNI spec implemented by this library
|
||||
func Current() string {
|
||||
return "0.4.0"
|
||||
}
|
||||
|
||||
// Legacy PluginInfo describes a plugin that is backwards compatible with the
|
||||
// CNI spec version 0.1.0. In particular, a runtime compiled against the 0.1.0
|
||||
// library ought to work correctly with a plugin that reports support for
|
||||
// Legacy versions.
|
||||
//
|
||||
// Any future CNI spec versions which meet this definition should be added to
|
||||
// this list.
|
||||
var Legacy = PluginSupports("0.1.0", "0.2.0")
|
||||
var All = PluginSupports("0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0")
|
||||
|
||||
var resultFactories = []struct {
|
||||
supportedVersions []string
|
||||
newResult types.ResultFactoryFunc
|
||||
}{
|
||||
{current.SupportedVersions, current.NewResult},
|
||||
{types020.SupportedVersions, types020.NewResult},
|
||||
}
|
||||
|
||||
// Finds a Result object matching the requested version (if any) and asks
|
||||
// that object to parse the plugin result, returning an error if parsing failed.
|
||||
func NewResult(version string, resultBytes []byte) (types.Result, error) {
|
||||
reconciler := &Reconciler{}
|
||||
for _, resultFactory := range resultFactories {
|
||||
err := reconciler.CheckRaw(version, resultFactory.supportedVersions)
|
||||
if err == nil {
|
||||
// Result supports this version
|
||||
return resultFactory.newResult(resultBytes)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported CNI result version %q", version)
|
||||
}
|
||||
|
||||
// ParsePrevResult parses a prevResult in a NetConf structure and sets
|
||||
// the NetConf's PrevResult member to the parsed Result object.
|
||||
func ParsePrevResult(conf *types.NetConf) error {
|
||||
if conf.RawPrevResult == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
resultBytes, err := json.Marshal(conf.RawPrevResult)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not serialize prevResult: %v", err)
|
||||
}
|
||||
|
||||
conf.RawPrevResult = nil
|
||||
conf.PrevResult, err = NewResult(conf.CNIVersion, resultBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse prevResult: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,41 @@
|
|||
### Namespaces, Threads, and Go
|
||||
On Linux each OS thread can have a different network namespace. Go's thread scheduling model switches goroutines between OS threads based on OS thread load and whether the goroutine would block other goroutines. This can result in a goroutine switching network namespaces without notice and lead to errors in your code.
|
||||
|
||||
### Namespace Switching
|
||||
Switching namespaces with the `ns.Set()` method is not recommended without additional strategies to prevent unexpected namespace changes when your goroutines switch OS threads.
|
||||
|
||||
Go provides the `runtime.LockOSThread()` function to ensure a specific goroutine executes on its current OS thread and prevents any other goroutine from running in that thread until the locked one exits. Careful usage of `LockOSThread()` and goroutines can provide good control over which network namespace a given goroutine executes in.
|
||||
|
||||
For example, you cannot rely on the `ns.Set()` namespace being the current namespace after the `Set()` call unless you do two things. First, the goroutine calling `Set()` must have previously called `LockOSThread()`. Second, you must ensure `runtime.UnlockOSThread()` is not called somewhere in-between. You also cannot rely on the initial network namespace remaining the current network namespace if any other code in your program switches namespaces, unless you have already called `LockOSThread()` in that goroutine. Note that `LockOSThread()` prevents the Go scheduler from optimally scheduling goroutines for best performance, so `LockOSThread()` should only be used in small, isolated goroutines that release the lock quickly.
|
||||
|
||||
### Do() The Recommended Thing
|
||||
The `ns.Do()` method provides **partial** control over network namespaces for you by implementing these strategies. All code dependent on a particular network namespace (including the root namespace) should be wrapped in the `ns.Do()` method to ensure the correct namespace is selected for the duration of your code. For example:
|
||||
|
||||
```go
|
||||
err = targetNs.Do(func(hostNs ns.NetNS) error {
|
||||
dummy := &netlink.Dummy{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: "dummy0",
|
||||
},
|
||||
}
|
||||
return netlink.LinkAdd(dummy)
|
||||
})
|
||||
```
|
||||
|
||||
Note this requirement to wrap every network call is very onerous - any libraries you call might call out to network services such as DNS, and all such calls need to be protected after you call `ns.Do()`. All goroutines spawned from within the `ns.Do` will not inherit the new namespace. The CNI plugins all exit very soon after calling `ns.Do()` which helps to minimize the problem.
|
||||
|
||||
When a new thread is spawned in Linux, it inherits the namespace of its parent. In versions of go **prior to 1.10**, if the runtime spawns a new OS thread, it picks the parent randomly. If the chosen parent thread has been moved to a new namespace (even temporarily), the new OS thread will be permanently "stuck in the wrong namespace", and goroutines will non-deterministically switch namespaces as they are rescheduled.
|
||||
|
||||
In short, **there was no safe way to change network namespaces, even temporarily, from within a long-lived, multithreaded Go process**. If you wish to do this, you must use go 1.10 or greater.
|
||||
|
||||
|
||||
### Creating network namespaces
|
||||
Earlier versions of this library managed namespace creation, but as CNI does not actually utilize this feature (and it was essentially unmaintained), it was removed. If you're writing a container runtime, you should implement namespace management yourself. However, there are some gotchas when doing so, especially around handling `/var/run/netns`. A reasonably correct reference implementation, borrowed from `rkt`, can be found in `pkg/testutils/netns_linux.go` if you're in need of a source of inspiration.
|
||||
|
||||
|
||||
### Further Reading
|
||||
- https://github.com/golang/go/wiki/LockOSThread
|
||||
- http://morsmachine.dk/go-scheduler
|
||||
- https://github.com/containernetworking/cni/issues/262
|
||||
- https://golang.org/pkg/runtime/
|
||||
- https://www.weave.works/blog/linux-namespaces-and-go-don-t-mix
|
|
@ -0,0 +1,216 @@
|
|||
// Copyright 2015-2017 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// Returns an object representing the current OS thread's network namespace
|
||||
func GetCurrentNS() (NetNS, error) {
|
||||
return GetNS(getCurrentThreadNetNSPath())
|
||||
}
|
||||
|
||||
func getCurrentThreadNetNSPath() string {
|
||||
// /proc/self/ns/net returns the namespace of the main thread, not
|
||||
// of whatever thread this goroutine is running on. Make sure we
|
||||
// use the thread's net namespace since the thread is switching around
|
||||
return fmt.Sprintf("/proc/%d/task/%d/ns/net", os.Getpid(), unix.Gettid())
|
||||
}
|
||||
|
||||
func (ns *netNS) Close() error {
|
||||
if err := ns.errorIfClosed(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ns.file.Close(); err != nil {
|
||||
return fmt.Errorf("Failed to close %q: %v", ns.file.Name(), err)
|
||||
}
|
||||
ns.closed = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ns *netNS) Set() error {
|
||||
if err := ns.errorIfClosed(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := unix.Setns(int(ns.Fd()), unix.CLONE_NEWNET); err != nil {
|
||||
return fmt.Errorf("Error switching to ns %v: %v", ns.file.Name(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type NetNS interface {
|
||||
// Executes the passed closure in this object's network namespace,
|
||||
// attempting to restore the original namespace before returning.
|
||||
// However, since each OS thread can have a different network namespace,
|
||||
// and Go's thread scheduling is highly variable, callers cannot
|
||||
// guarantee any specific namespace is set unless operations that
|
||||
// require that namespace are wrapped with Do(). Also, no code called
|
||||
// from Do() should call runtime.UnlockOSThread(), or the risk
|
||||
// of executing code in an incorrect namespace will be greater. See
|
||||
// https://github.com/golang/go/wiki/LockOSThread for further details.
|
||||
Do(toRun func(NetNS) error) error
|
||||
|
||||
// Sets the current network namespace to this object's network namespace.
|
||||
// Note that since Go's thread scheduling is highly variable, callers
|
||||
// cannot guarantee the requested namespace will be the current namespace
|
||||
// after this function is called; to ensure this wrap operations that
|
||||
// require the namespace with Do() instead.
|
||||
Set() error
|
||||
|
||||
// Returns the filesystem path representing this object's network namespace
|
||||
Path() string
|
||||
|
||||
// Returns a file descriptor representing this object's network namespace
|
||||
Fd() uintptr
|
||||
|
||||
// Cleans up this instance of the network namespace; if this instance
|
||||
// is the last user the namespace will be destroyed
|
||||
Close() error
|
||||
}
|
||||
|
||||
type netNS struct {
|
||||
file *os.File
|
||||
closed bool
|
||||
}
|
||||
|
||||
// netNS implements the NetNS interface
|
||||
var _ NetNS = &netNS{}
|
||||
|
||||
const (
|
||||
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/magic.h
|
||||
NSFS_MAGIC = 0x6e736673
|
||||
PROCFS_MAGIC = 0x9fa0
|
||||
)
|
||||
|
||||
type NSPathNotExistErr struct{ msg string }
|
||||
|
||||
func (e NSPathNotExistErr) Error() string { return e.msg }
|
||||
|
||||
type NSPathNotNSErr struct{ msg string }
|
||||
|
||||
func (e NSPathNotNSErr) Error() string { return e.msg }
|
||||
|
||||
func IsNSorErr(nspath string) error {
|
||||
stat := syscall.Statfs_t{}
|
||||
if err := syscall.Statfs(nspath, &stat); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = NSPathNotExistErr{msg: fmt.Sprintf("failed to Statfs %q: %v", nspath, err)}
|
||||
} else {
|
||||
err = fmt.Errorf("failed to Statfs %q: %v", nspath, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
switch stat.Type {
|
||||
case PROCFS_MAGIC, NSFS_MAGIC:
|
||||
return nil
|
||||
default:
|
||||
return NSPathNotNSErr{msg: fmt.Sprintf("unknown FS magic on %q: %x", nspath, stat.Type)}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns an object representing the namespace referred to by @path
|
||||
func GetNS(nspath string) (NetNS, error) {
|
||||
err := IsNSorErr(nspath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fd, err := os.Open(nspath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &netNS{file: fd}, nil
|
||||
}
|
||||
|
||||
func (ns *netNS) Path() string {
|
||||
return ns.file.Name()
|
||||
}
|
||||
|
||||
func (ns *netNS) Fd() uintptr {
|
||||
return ns.file.Fd()
|
||||
}
|
||||
|
||||
func (ns *netNS) errorIfClosed() error {
|
||||
if ns.closed {
|
||||
return fmt.Errorf("%q has already been closed", ns.file.Name())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ns *netNS) Do(toRun func(NetNS) error) error {
|
||||
if err := ns.errorIfClosed(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
containedCall := func(hostNS NetNS) error {
|
||||
threadNS, err := GetCurrentNS()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open current netns: %v", err)
|
||||
}
|
||||
defer threadNS.Close()
|
||||
|
||||
// switch to target namespace
|
||||
if err = ns.Set(); err != nil {
|
||||
return fmt.Errorf("error switching to ns %v: %v", ns.file.Name(), err)
|
||||
}
|
||||
defer threadNS.Set() // switch back
|
||||
|
||||
return toRun(hostNS)
|
||||
}
|
||||
|
||||
// save a handle to current network namespace
|
||||
hostNS, err := GetCurrentNS()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to open current namespace: %v", err)
|
||||
}
|
||||
defer hostNS.Close()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
var innerError error
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
runtime.LockOSThread()
|
||||
innerError = containedCall(hostNS)
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
return innerError
|
||||
}
|
||||
|
||||
// WithNetNSPath executes the passed closure under the given network
|
||||
// namespace, restoring the original namespace afterwards.
|
||||
func WithNetNSPath(nspath string, toRun func(NetNS) error) error {
|
||||
ns, err := GetNS(nspath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ns.Close()
|
||||
return ns.Do(toRun)
|
||||
}
|
|
@ -17,27 +17,51 @@ Usage
|
|||
Below is an example of using the Consul client:
|
||||
|
||||
```go
|
||||
// Get a new client
|
||||
client, err := api.NewClient(api.DefaultConfig())
|
||||
if err != nil {
|
||||
package main
|
||||
|
||||
import "github.com/hashicorp/consul/api"
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
// Get a new client
|
||||
client, err := api.NewClient(api.DefaultConfig())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get a handle to the KV API
|
||||
kv := client.KV()
|
||||
// Get a handle to the KV API
|
||||
kv := client.KV()
|
||||
|
||||
// PUT a new KV pair
|
||||
p := &api.KVPair{Key: "foo", Value: []byte("test")}
|
||||
_, err = kv.Put(p, nil)
|
||||
if err != nil {
|
||||
// PUT a new KV pair
|
||||
p := &api.KVPair{Key: "REDIS_MAXCLIENTS", Value: []byte("1000")}
|
||||
_, err = kv.Put(p, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup the pair
|
||||
pair, _, err := kv.Get("foo", nil)
|
||||
if err != nil {
|
||||
// Lookup the pair
|
||||
pair, _, err := kv.Get("REDIS_MAXCLIENTS", nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("KV: %v %s\n", pair.Key, pair.Value)
|
||||
}
|
||||
fmt.Printf("KV: %v", pair)
|
||||
|
||||
```
|
||||
|
||||
To run this example, start a Consul server:
|
||||
|
||||
```bash
|
||||
consul agent -dev
|
||||
```
|
||||
|
||||
Copy the code above into a file such as `main.go`.
|
||||
|
||||
Install and run. You'll see a key (`REDIS_MAXCLIENTS`) and value (`1000`) printed.
|
||||
|
||||
```bash
|
||||
$ go get
|
||||
$ go run main.go
|
||||
KV: REDIS_MAXCLIENTS 1000
|
||||
```
|
||||
|
||||
After running the code, you can also view the values in the Consul UI on your local machine at http://localhost:8500/ui/dc1/kv
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2,7 +2,11 @@ package api
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// ServiceKind is the kind of service being registered.
|
||||
|
@ -38,6 +42,18 @@ const (
|
|||
ProxyExecModeScript ProxyExecMode = "script"
|
||||
)
|
||||
|
||||
// UpstreamDestType is the type of upstream discovery mechanism.
|
||||
type UpstreamDestType string
|
||||
|
||||
const (
|
||||
// UpstreamDestTypeService discovers instances via healthy service lookup.
|
||||
UpstreamDestTypeService UpstreamDestType = "service"
|
||||
|
||||
// UpstreamDestTypePreparedQuery discovers instances via prepared query
|
||||
// execution.
|
||||
UpstreamDestTypePreparedQuery UpstreamDestType = "prepared_query"
|
||||
)
|
||||
|
||||
// AgentCheck represents a check known to the agent
|
||||
type AgentCheck struct {
|
||||
Node string
|
||||
|
@ -51,34 +67,64 @@ type AgentCheck struct {
|
|||
Definition HealthCheckDefinition
|
||||
}
|
||||
|
||||
// AgentWeights represent optional weights for a service
|
||||
type AgentWeights struct {
|
||||
Passing int
|
||||
Warning int
|
||||
}
|
||||
|
||||
// AgentService represents a service known to the agent
|
||||
type AgentService struct {
|
||||
Kind ServiceKind
|
||||
Kind ServiceKind `json:",omitempty"`
|
||||
ID string
|
||||
Service string
|
||||
Tags []string
|
||||
Meta map[string]string
|
||||
Port int
|
||||
Address string
|
||||
Weights AgentWeights
|
||||
EnableTagOverride bool
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
ProxyDestination string
|
||||
Connect *AgentServiceConnect
|
||||
CreateIndex uint64 `json:",omitempty" bexpr:"-"`
|
||||
ModifyIndex uint64 `json:",omitempty" bexpr:"-"`
|
||||
ContentHash string `json:",omitempty" bexpr:"-"`
|
||||
// DEPRECATED (ProxyDestination) - remove this field
|
||||
ProxyDestination string `json:",omitempty" bexpr:"-"`
|
||||
Proxy *AgentServiceConnectProxyConfig `json:",omitempty"`
|
||||
Connect *AgentServiceConnect `json:",omitempty"`
|
||||
}
|
||||
|
||||
// AgentServiceChecksInfo returns information about a Service and its checks
|
||||
type AgentServiceChecksInfo struct {
|
||||
AggregatedStatus string
|
||||
Service *AgentService
|
||||
Checks HealthChecks
|
||||
}
|
||||
|
||||
// AgentServiceConnect represents the Connect configuration of a service.
|
||||
type AgentServiceConnect struct {
|
||||
Native bool
|
||||
Proxy *AgentServiceConnectProxy
|
||||
Native bool `json:",omitempty"`
|
||||
Proxy *AgentServiceConnectProxy `json:",omitempty" bexpr:"-"`
|
||||
SidecarService *AgentServiceRegistration `json:",omitempty" bexpr:"-"`
|
||||
}
|
||||
|
||||
// AgentServiceConnectProxy represents the Connect Proxy configuration of a
|
||||
// service.
|
||||
type AgentServiceConnectProxy struct {
|
||||
ExecMode ProxyExecMode
|
||||
Command []string
|
||||
Config map[string]interface{}
|
||||
ExecMode ProxyExecMode `json:",omitempty"`
|
||||
Command []string `json:",omitempty"`
|
||||
Config map[string]interface{} `json:",omitempty" bexpr:"-"`
|
||||
Upstreams []Upstream `json:",omitempty"`
|
||||
}
|
||||
|
||||
// AgentServiceConnectProxyConfig is the proxy configuration in a connect-proxy
|
||||
// ServiceDefinition or response.
|
||||
type AgentServiceConnectProxyConfig struct {
|
||||
DestinationServiceName string
|
||||
DestinationServiceID string `json:",omitempty"`
|
||||
LocalServiceAddress string `json:",omitempty"`
|
||||
LocalServicePort int `json:",omitempty"`
|
||||
Config map[string]interface{} `json:",omitempty" bexpr:"-"`
|
||||
Upstreams []Upstream
|
||||
}
|
||||
|
||||
// AgentMember represents a cluster member known to the agent
|
||||
|
@ -119,9 +165,12 @@ type AgentServiceRegistration struct {
|
|||
Address string `json:",omitempty"`
|
||||
EnableTagOverride bool `json:",omitempty"`
|
||||
Meta map[string]string `json:",omitempty"`
|
||||
Weights *AgentWeights `json:",omitempty"`
|
||||
Check *AgentServiceCheck
|
||||
Checks AgentServiceChecks
|
||||
// DEPRECATED (ProxyDestination) - remove this field
|
||||
ProxyDestination string `json:",omitempty"`
|
||||
Proxy *AgentServiceConnectProxyConfig `json:",omitempty"`
|
||||
Connect *AgentServiceConnect `json:",omitempty"`
|
||||
}
|
||||
|
||||
|
@ -153,6 +202,8 @@ type AgentServiceCheck struct {
|
|||
TLSSkipVerify bool `json:",omitempty"`
|
||||
GRPC string `json:",omitempty"`
|
||||
GRPCUseTLS bool `json:",omitempty"`
|
||||
AliasNode string `json:",omitempty"`
|
||||
AliasService string `json:",omitempty"`
|
||||
|
||||
// In Consul 0.7 and later, checks that are associated with a service
|
||||
// may also contain this optional DeregisterCriticalServiceAfter field,
|
||||
|
@ -225,9 +276,23 @@ type ConnectProxyConfig struct {
|
|||
TargetServiceID string
|
||||
TargetServiceName string
|
||||
ContentHash string
|
||||
ExecMode ProxyExecMode
|
||||
Command []string
|
||||
Config map[string]interface{}
|
||||
// DEPRECATED(managed-proxies) - this struct is re-used for sidecar configs
|
||||
// but they don't need ExecMode or Command
|
||||
ExecMode ProxyExecMode `json:",omitempty"`
|
||||
Command []string `json:",omitempty"`
|
||||
Config map[string]interface{} `bexpr:"-"`
|
||||
Upstreams []Upstream
|
||||
}
|
||||
|
||||
// Upstream is the response structure for a proxy upstream configuration.
|
||||
type Upstream struct {
|
||||
DestinationType UpstreamDestType `json:",omitempty"`
|
||||
DestinationNamespace string `json:",omitempty"`
|
||||
DestinationName string
|
||||
Datacenter string `json:",omitempty"`
|
||||
LocalBindAddress string `json:",omitempty"`
|
||||
LocalBindPort int `json:",omitempty"`
|
||||
Config map[string]interface{} `json:",omitempty" bexpr:"-"`
|
||||
}
|
||||
|
||||
// Agent can be used to query the Agent endpoints
|
||||
|
@ -260,6 +325,24 @@ func (a *Agent) Self() (map[string]map[string]interface{}, error) {
|
|||
return out, nil
|
||||
}
|
||||
|
||||
// Host is used to retrieve information about the host the
|
||||
// agent is running on such as CPU, memory, and disk. Requires
|
||||
// a operator:read ACL token.
|
||||
func (a *Agent) Host() (map[string]interface{}, error) {
|
||||
r := a.c.newRequest("GET", "/v1/agent/host")
|
||||
_, resp, err := requireOK(a.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var out map[string]interface{}
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Metrics is used to query the agent we are speaking to for
|
||||
// its current internal metric data
|
||||
func (a *Agent) Metrics() (*MetricsInfo, error) {
|
||||
|
@ -304,7 +387,14 @@ func (a *Agent) NodeName() (string, error) {
|
|||
|
||||
// Checks returns the locally registered checks
|
||||
func (a *Agent) Checks() (map[string]*AgentCheck, error) {
|
||||
return a.ChecksWithFilter("")
|
||||
}
|
||||
|
||||
// ChecksWithFilter returns a subset of the locally registered checks that match
|
||||
// the given filter expression
|
||||
func (a *Agent) ChecksWithFilter(filter string) (map[string]*AgentCheck, error) {
|
||||
r := a.c.newRequest("GET", "/v1/agent/checks")
|
||||
r.filterQuery(filter)
|
||||
_, resp, err := requireOK(a.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -320,7 +410,14 @@ func (a *Agent) Checks() (map[string]*AgentCheck, error) {
|
|||
|
||||
// Services returns the locally registered services
|
||||
func (a *Agent) Services() (map[string]*AgentService, error) {
|
||||
return a.ServicesWithFilter("")
|
||||
}
|
||||
|
||||
// ServicesWithFilter returns a subset of the locally registered services that match
|
||||
// the given filter expression
|
||||
func (a *Agent) ServicesWithFilter(filter string) (map[string]*AgentService, error) {
|
||||
r := a.c.newRequest("GET", "/v1/agent/services")
|
||||
r.filterQuery(filter)
|
||||
_, resp, err := requireOK(a.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -335,6 +432,100 @@ func (a *Agent) Services() (map[string]*AgentService, error) {
|
|||
return out, nil
|
||||
}
|
||||
|
||||
// AgentHealthServiceByID returns for a given serviceID: the aggregated health status, the service definition or an error if any
|
||||
// - If the service is not found, will return status (critical, nil, nil)
|
||||
// - If the service is found, will return (critical|passing|warning), AgentServiceChecksInfo, nil)
|
||||
// - In all other cases, will return an error
|
||||
func (a *Agent) AgentHealthServiceByID(serviceID string) (string, *AgentServiceChecksInfo, error) {
|
||||
path := fmt.Sprintf("/v1/agent/health/service/id/%v", url.PathEscape(serviceID))
|
||||
r := a.c.newRequest("GET", path)
|
||||
r.params.Add("format", "json")
|
||||
r.header.Set("Accept", "application/json")
|
||||
_, resp, err := a.c.doRequest(r)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
// Service not Found
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return HealthCritical, nil, nil
|
||||
}
|
||||
var out *AgentServiceChecksInfo
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return HealthCritical, out, err
|
||||
}
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return HealthPassing, out, nil
|
||||
case http.StatusTooManyRequests:
|
||||
return HealthWarning, out, nil
|
||||
case http.StatusServiceUnavailable:
|
||||
return HealthCritical, out, nil
|
||||
}
|
||||
return HealthCritical, out, fmt.Errorf("Unexpected Error Code %v for %s", resp.StatusCode, path)
|
||||
}
|
||||
|
||||
// AgentHealthServiceByName returns for a given service name: the aggregated health status for all services
|
||||
// having the specified name.
|
||||
// - If no service is not found, will return status (critical, [], nil)
|
||||
// - If the service is found, will return (critical|passing|warning), []api.AgentServiceChecksInfo, nil)
|
||||
// - In all other cases, will return an error
|
||||
func (a *Agent) AgentHealthServiceByName(service string) (string, []AgentServiceChecksInfo, error) {
|
||||
path := fmt.Sprintf("/v1/agent/health/service/name/%v", url.PathEscape(service))
|
||||
r := a.c.newRequest("GET", path)
|
||||
r.params.Add("format", "json")
|
||||
r.header.Set("Accept", "application/json")
|
||||
_, resp, err := a.c.doRequest(r)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
// Service not Found
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return HealthCritical, nil, nil
|
||||
}
|
||||
var out []AgentServiceChecksInfo
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return HealthCritical, out, err
|
||||
}
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return HealthPassing, out, nil
|
||||
case http.StatusTooManyRequests:
|
||||
return HealthWarning, out, nil
|
||||
case http.StatusServiceUnavailable:
|
||||
return HealthCritical, out, nil
|
||||
}
|
||||
return HealthCritical, out, fmt.Errorf("Unexpected Error Code %v for %s", resp.StatusCode, path)
|
||||
}
|
||||
|
||||
// Service returns a locally registered service instance and allows for
|
||||
// hash-based blocking.
|
||||
//
|
||||
// Note that this uses an unconventional blocking mechanism since it's
|
||||
// agent-local state. That means there is no persistent raft index so we block
|
||||
// based on object hash instead.
|
||||
func (a *Agent) Service(serviceID string, q *QueryOptions) (*AgentService, *QueryMeta, error) {
|
||||
r := a.c.newRequest("GET", "/v1/agent/service/"+serviceID)
|
||||
r.setQueryOptions(q)
|
||||
rtt, resp, err := requireOK(a.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var out *AgentService
|
||||
if err := decodeBody(resp, &out); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return out, qm, nil
|
||||
}
|
||||
|
||||
// Members returns the known gossip members. The WAN
|
||||
// flag can be used to query a server for WAN members.
|
||||
func (a *Agent) Members(wan bool) ([]*AgentMember, error) {
|
||||
|
@ -751,41 +942,94 @@ func (a *Agent) Monitor(loglevel string, stopCh <-chan struct{}, q *QueryOptions
|
|||
|
||||
// UpdateACLToken updates the agent's "acl_token". See updateToken for more
|
||||
// details.
|
||||
//
|
||||
// DEPRECATED (ACL-Legacy-Compat) - Prefer UpdateDefaultACLToken for v1.4.3 and above
|
||||
func (a *Agent) UpdateACLToken(token string, q *WriteOptions) (*WriteMeta, error) {
|
||||
return a.updateToken("acl_token", token, q)
|
||||
}
|
||||
|
||||
// UpdateACLAgentToken updates the agent's "acl_agent_token". See updateToken
|
||||
// for more details.
|
||||
//
|
||||
// DEPRECATED (ACL-Legacy-Compat) - Prefer UpdateAgentACLToken for v1.4.3 and above
|
||||
func (a *Agent) UpdateACLAgentToken(token string, q *WriteOptions) (*WriteMeta, error) {
|
||||
return a.updateToken("acl_agent_token", token, q)
|
||||
}
|
||||
|
||||
// UpdateACLAgentMasterToken updates the agent's "acl_agent_master_token". See
|
||||
// updateToken for more details.
|
||||
//
|
||||
// DEPRECATED (ACL-Legacy-Compat) - Prefer UpdateAgentMasterACLToken for v1.4.3 and above
|
||||
func (a *Agent) UpdateACLAgentMasterToken(token string, q *WriteOptions) (*WriteMeta, error) {
|
||||
return a.updateToken("acl_agent_master_token", token, q)
|
||||
}
|
||||
|
||||
// UpdateACLReplicationToken updates the agent's "acl_replication_token". See
|
||||
// updateToken for more details.
|
||||
//
|
||||
// DEPRECATED (ACL-Legacy-Compat) - Prefer UpdateReplicationACLToken for v1.4.3 and above
|
||||
func (a *Agent) UpdateACLReplicationToken(token string, q *WriteOptions) (*WriteMeta, error) {
|
||||
return a.updateToken("acl_replication_token", token, q)
|
||||
}
|
||||
|
||||
// updateToken can be used to update an agent's ACL token after the agent has
|
||||
// started. The tokens are not persisted, so will need to be updated again if
|
||||
// the agent is restarted.
|
||||
// UpdateDefaultACLToken updates the agent's "default" token. See updateToken
|
||||
// for more details
|
||||
func (a *Agent) UpdateDefaultACLToken(token string, q *WriteOptions) (*WriteMeta, error) {
|
||||
return a.updateTokenFallback("default", "acl_token", token, q)
|
||||
}
|
||||
|
||||
// UpdateAgentACLToken updates the agent's "agent" token. See updateToken
|
||||
// for more details
|
||||
func (a *Agent) UpdateAgentACLToken(token string, q *WriteOptions) (*WriteMeta, error) {
|
||||
return a.updateTokenFallback("agent", "acl_agent_token", token, q)
|
||||
}
|
||||
|
||||
// UpdateAgentMasterACLToken updates the agent's "agent_master" token. See updateToken
|
||||
// for more details
|
||||
func (a *Agent) UpdateAgentMasterACLToken(token string, q *WriteOptions) (*WriteMeta, error) {
|
||||
return a.updateTokenFallback("agent_master", "acl_agent_master_token", token, q)
|
||||
}
|
||||
|
||||
// UpdateReplicationACLToken updates the agent's "replication" token. See updateToken
|
||||
// for more details
|
||||
func (a *Agent) UpdateReplicationACLToken(token string, q *WriteOptions) (*WriteMeta, error) {
|
||||
return a.updateTokenFallback("replication", "acl_replication_token", token, q)
|
||||
}
|
||||
|
||||
// updateToken can be used to update one of an agent's ACL tokens after the agent has
|
||||
// started. The tokens are may not be persisted, so will need to be updated again if
|
||||
// the agent is restarted unless the agent is configured to persist them.
|
||||
func (a *Agent) updateToken(target, token string, q *WriteOptions) (*WriteMeta, error) {
|
||||
meta, _, err := a.updateTokenOnce(target, token, q)
|
||||
return meta, err
|
||||
}
|
||||
|
||||
func (a *Agent) updateTokenFallback(target, fallback, token string, q *WriteOptions) (*WriteMeta, error) {
|
||||
meta, status, err := a.updateTokenOnce(target, token, q)
|
||||
if err != nil && status == 404 {
|
||||
meta, _, err = a.updateTokenOnce(fallback, token, q)
|
||||
}
|
||||
return meta, err
|
||||
}
|
||||
|
||||
func (a *Agent) updateTokenOnce(target, token string, q *WriteOptions) (*WriteMeta, int, error) {
|
||||
r := a.c.newRequest("PUT", fmt.Sprintf("/v1/agent/token/%s", target))
|
||||
r.setWriteOptions(q)
|
||||
r.obj = &AgentToken{Token: token}
|
||||
rtt, resp, err := requireOK(a.c.doRequest(r))
|
||||
|
||||
rtt, resp, err := a.c.doRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, 0, err
|
||||
}
|
||||
resp.Body.Close()
|
||||
defer resp.Body.Close()
|
||||
|
||||
wm := &WriteMeta{RequestTime: rtt}
|
||||
return wm, nil
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, resp.Body)
|
||||
return wm, resp.StatusCode, fmt.Errorf("Unexpected response code: %d (%s)", resp.StatusCode, buf.Bytes())
|
||||
}
|
||||
|
||||
return wm, resp.StatusCode, nil
|
||||
}
|
||||
|
|
|
@ -30,6 +30,10 @@ const (
|
|||
// the HTTP token.
|
||||
HTTPTokenEnvName = "CONSUL_HTTP_TOKEN"
|
||||
|
||||
// HTTPTokenFileEnvName defines an environment variable name which sets
|
||||
// the HTTP token file.
|
||||
HTTPTokenFileEnvName = "CONSUL_HTTP_TOKEN_FILE"
|
||||
|
||||
// HTTPAuthEnvName defines an environment variable name which sets
|
||||
// the HTTP authentication header.
|
||||
HTTPAuthEnvName = "CONSUL_HTTP_AUTH"
|
||||
|
@ -61,6 +65,12 @@ const (
|
|||
// HTTPSSLVerifyEnvName defines an environment variable name which sets
|
||||
// whether or not to disable certificate checking.
|
||||
HTTPSSLVerifyEnvName = "CONSUL_HTTP_SSL_VERIFY"
|
||||
|
||||
// GRPCAddrEnvName defines an environment variable name which sets the gRPC
|
||||
// address for consul connect envoy. Note this isn't actually used by the api
|
||||
// client in this package but is defined here for consistency with all the
|
||||
// other ENV names we use.
|
||||
GRPCAddrEnvName = "CONSUL_GRPC_ADDR"
|
||||
)
|
||||
|
||||
// QueryOptions are used to parameterize a query
|
||||
|
@ -78,6 +88,27 @@ type QueryOptions struct {
|
|||
// read.
|
||||
RequireConsistent bool
|
||||
|
||||
// UseCache requests that the agent cache results locally. See
|
||||
// https://www.consul.io/api/index.html#agent-caching for more details on the
|
||||
// semantics.
|
||||
UseCache bool
|
||||
|
||||
// MaxAge limits how old a cached value will be returned if UseCache is true.
|
||||
// If there is a cached response that is older than the MaxAge, it is treated
|
||||
// as a cache miss and a new fetch invoked. If the fetch fails, the error is
|
||||
// returned. Clients that wish to allow for stale results on error can set
|
||||
// StaleIfError to a longer duration to change this behavior. It is ignored
|
||||
// if the endpoint supports background refresh caching. See
|
||||
// https://www.consul.io/api/index.html#agent-caching for more details.
|
||||
MaxAge time.Duration
|
||||
|
||||
// StaleIfError specifies how stale the client will accept a cached response
|
||||
// if the servers are unavailable to fetch a fresh one. Only makes sense when
|
||||
// UseCache is true and MaxAge is set to a lower, non-zero value. It is
|
||||
// ignored if the endpoint supports background refresh caching. See
|
||||
// https://www.consul.io/api/index.html#agent-caching for more details.
|
||||
StaleIfError time.Duration
|
||||
|
||||
// WaitIndex is used to enable a blocking query. Waits
|
||||
// until the timeout or the next index is reached
|
||||
WaitIndex uint64
|
||||
|
@ -119,6 +150,10 @@ type QueryOptions struct {
|
|||
// ctx is an optional context pass through to the underlying HTTP
|
||||
// request layer. Use Context() and WithContext() to manage this.
|
||||
ctx context.Context
|
||||
|
||||
// Filter requests filtering data prior to it being returned. The string
|
||||
// is a go-bexpr compatible expression.
|
||||
Filter string
|
||||
}
|
||||
|
||||
func (o *QueryOptions) Context() context.Context {
|
||||
|
@ -196,6 +231,13 @@ type QueryMeta struct {
|
|||
|
||||
// Is address translation enabled for HTTP responses on this agent
|
||||
AddressTranslationEnabled bool
|
||||
|
||||
// CacheHit is true if the result was served from agent-local cache.
|
||||
CacheHit bool
|
||||
|
||||
// CacheAge is set if request was ?cached and indicates how stale the cached
|
||||
// response is.
|
||||
CacheAge time.Duration
|
||||
}
|
||||
|
||||
// WriteMeta is used to return meta data about a write
|
||||
|
@ -242,6 +284,10 @@ type Config struct {
|
|||
// which overrides the agent's default token.
|
||||
Token string
|
||||
|
||||
// TokenFile is a file containing the current token to use for this client.
|
||||
// If provided it is read once at startup and never again.
|
||||
TokenFile string
|
||||
|
||||
TLSConfig TLSConfig
|
||||
}
|
||||
|
||||
|
@ -276,7 +322,7 @@ type TLSConfig struct {
|
|||
// DefaultConfig returns a default configuration for the client. By default this
|
||||
// will pool and reuse idle connections to Consul. If you have a long-lived
|
||||
// client object, this is the desired behavior and should make the most efficient
|
||||
// use of the connections to Consul. If you don't reuse a client object , which
|
||||
// use of the connections to Consul. If you don't reuse a client object, which
|
||||
// is not recommended, then you may notice idle connections building up over
|
||||
// time. To avoid this, use the DefaultNonPooledConfig() instead.
|
||||
func DefaultConfig() *Config {
|
||||
|
@ -305,6 +351,10 @@ func defaultConfig(transportFn func() *http.Transport) *Config {
|
|||
config.Address = addr
|
||||
}
|
||||
|
||||
if tokenFile := os.Getenv(HTTPTokenFileEnvName); tokenFile != "" {
|
||||
config.TokenFile = tokenFile
|
||||
}
|
||||
|
||||
if token := os.Getenv(HTTPTokenEnvName); token != "" {
|
||||
config.Token = token
|
||||
}
|
||||
|
@ -411,6 +461,7 @@ func (c *Config) GenerateEnv() []string {
|
|||
env = append(env,
|
||||
fmt.Sprintf("%s=%s", HTTPAddrEnvName, c.Address),
|
||||
fmt.Sprintf("%s=%s", HTTPTokenEnvName, c.Token),
|
||||
fmt.Sprintf("%s=%s", HTTPTokenFileEnvName, c.TokenFile),
|
||||
fmt.Sprintf("%s=%t", HTTPSSLEnvName, c.Scheme == "https"),
|
||||
fmt.Sprintf("%s=%s", HTTPCAFile, c.TLSConfig.CAFile),
|
||||
fmt.Sprintf("%s=%s", HTTPCAPath, c.TLSConfig.CAPath),
|
||||
|
@ -503,6 +554,19 @@ func NewClient(config *Config) (*Client, error) {
|
|||
config.Address = parts[1]
|
||||
}
|
||||
|
||||
// If the TokenFile is set, always use that, even if a Token is configured.
|
||||
// This is because when TokenFile is set it is read into the Token field.
|
||||
// We want any derived clients to have to re-read the token file.
|
||||
if config.TokenFile != "" {
|
||||
data, err := ioutil.ReadFile(config.TokenFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error loading token file: %s", err)
|
||||
}
|
||||
|
||||
if token := strings.TrimSpace(string(data)); token != "" {
|
||||
config.Token = token
|
||||
}
|
||||
}
|
||||
if config.Token == "" {
|
||||
config.Token = defConfig.Token
|
||||
}
|
||||
|
@ -580,6 +644,9 @@ func (r *request) setQueryOptions(q *QueryOptions) {
|
|||
if q.Near != "" {
|
||||
r.params.Set("near", q.Near)
|
||||
}
|
||||
if q.Filter != "" {
|
||||
r.params.Set("filter", q.Filter)
|
||||
}
|
||||
if len(q.NodeMeta) > 0 {
|
||||
for key, value := range q.NodeMeta {
|
||||
r.params.Add("node-meta", key+":"+value)
|
||||
|
@ -591,6 +658,20 @@ func (r *request) setQueryOptions(q *QueryOptions) {
|
|||
if q.Connect {
|
||||
r.params.Set("connect", "true")
|
||||
}
|
||||
if q.UseCache && !q.RequireConsistent {
|
||||
r.params.Set("cached", "")
|
||||
|
||||
cc := []string{}
|
||||
if q.MaxAge > 0 {
|
||||
cc = append(cc, fmt.Sprintf("max-age=%.0f", q.MaxAge.Seconds()))
|
||||
}
|
||||
if q.StaleIfError > 0 {
|
||||
cc = append(cc, fmt.Sprintf("stale-if-error=%.0f", q.StaleIfError.Seconds()))
|
||||
}
|
||||
if len(cc) > 0 {
|
||||
r.header.Set("Cache-Control", strings.Join(cc, ", "))
|
||||
}
|
||||
}
|
||||
r.ctx = q.ctx
|
||||
}
|
||||
|
||||
|
@ -725,7 +806,7 @@ func (c *Client) doRequest(r *request) (time.Duration, *http.Response, error) {
|
|||
func (c *Client) query(endpoint string, out interface{}, q *QueryOptions) (*QueryMeta, error) {
|
||||
r := c.newRequest("GET", endpoint)
|
||||
r.setQueryOptions(q)
|
||||
rtt, resp, err := requireOK(c.doRequest(r))
|
||||
rtt, resp, err := c.doRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -765,6 +846,8 @@ func (c *Client) write(endpoint string, in, out interface{}, q *WriteOptions) (*
|
|||
}
|
||||
|
||||
// parseQueryMeta is used to help parse query meta-data
|
||||
//
|
||||
// TODO(rb): bug? the error from this function is never handled
|
||||
func parseQueryMeta(resp *http.Response, q *QueryMeta) error {
|
||||
header := resp.Header
|
||||
|
||||
|
@ -802,6 +885,18 @@ func parseQueryMeta(resp *http.Response, q *QueryMeta) error {
|
|||
q.AddressTranslationEnabled = false
|
||||
}
|
||||
|
||||
// Parse Cache info
|
||||
if cacheStr := header.Get("X-Cache"); cacheStr != "" {
|
||||
q.CacheHit = strings.EqualFold(cacheStr, "HIT")
|
||||
}
|
||||
if ageStr := header.Get("Age"); ageStr != "" {
|
||||
age, err := strconv.ParseUint(ageStr, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse Age Header: %v", err)
|
||||
}
|
||||
q.CacheAge = time.Duration(age) * time.Second
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -830,10 +925,42 @@ func requireOK(d time.Duration, resp *http.Response, e error) (time.Duration, *h
|
|||
return d, nil, e
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, resp.Body)
|
||||
resp.Body.Close()
|
||||
return d, nil, fmt.Errorf("Unexpected response code: %d (%s)", resp.StatusCode, buf.Bytes())
|
||||
return d, nil, generateUnexpectedResponseCodeError(resp)
|
||||
}
|
||||
return d, resp, nil
|
||||
}
|
||||
|
||||
func (req *request) filterQuery(filter string) {
|
||||
if filter == "" {
|
||||
return
|
||||
}
|
||||
|
||||
req.params.Set("filter", filter)
|
||||
}
|
||||
|
||||
// generateUnexpectedResponseCodeError consumes the rest of the body, closes
|
||||
// the body stream and generates an error indicating the status code was
|
||||
// unexpected.
|
||||
func generateUnexpectedResponseCodeError(resp *http.Response) error {
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, resp.Body)
|
||||
resp.Body.Close()
|
||||
return fmt.Errorf("Unexpected response code: %d (%s)", resp.StatusCode, buf.Bytes())
|
||||
}
|
||||
|
||||
func requireNotFoundOrOK(d time.Duration, resp *http.Response, e error) (bool, time.Duration, *http.Response, error) {
|
||||
if e != nil {
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
return false, d, nil, e
|
||||
}
|
||||
switch resp.StatusCode {
|
||||
case 200:
|
||||
return true, d, resp, nil
|
||||
case 404:
|
||||
return false, d, resp, nil
|
||||
default:
|
||||
return false, d, nil, generateUnexpectedResponseCodeError(resp)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
package api
|
||||
|
||||
type Weights struct {
|
||||
Passing int
|
||||
Warning int
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
ID string
|
||||
Node string
|
||||
|
@ -24,8 +29,13 @@ type CatalogService struct {
|
|||
ServiceTags []string
|
||||
ServiceMeta map[string]string
|
||||
ServicePort int
|
||||
ServiceWeights Weights
|
||||
ServiceEnableTagOverride bool
|
||||
// DEPRECATED (ProxyDestination) - remove the next comment!
|
||||
// We forgot to ever add ServiceProxyDestination here so no need to deprecate!
|
||||
ServiceProxy *AgentServiceConnectProxyConfig
|
||||
CreateIndex uint64
|
||||
Checks HealthChecks
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
|
@ -43,6 +53,7 @@ type CatalogRegistration struct {
|
|||
Datacenter string
|
||||
Service *AgentService
|
||||
Check *AgentCheck
|
||||
Checks HealthChecks
|
||||
SkipNodeUpdate bool
|
||||
}
|
||||
|
||||
|
@ -156,23 +167,43 @@ func (c *Catalog) Services(q *QueryOptions) (map[string][]string, *QueryMeta, er
|
|||
|
||||
// Service is used to query catalog entries for a given service
|
||||
func (c *Catalog) Service(service, tag string, q *QueryOptions) ([]*CatalogService, *QueryMeta, error) {
|
||||
return c.service(service, tag, q, false)
|
||||
var tags []string
|
||||
if tag != "" {
|
||||
tags = []string{tag}
|
||||
}
|
||||
return c.service(service, tags, q, false)
|
||||
}
|
||||
|
||||
// Supports multiple tags for filtering
|
||||
func (c *Catalog) ServiceMultipleTags(service string, tags []string, q *QueryOptions) ([]*CatalogService, *QueryMeta, error) {
|
||||
return c.service(service, tags, q, false)
|
||||
}
|
||||
|
||||
// Connect is used to query catalog entries for a given Connect-enabled service
|
||||
func (c *Catalog) Connect(service, tag string, q *QueryOptions) ([]*CatalogService, *QueryMeta, error) {
|
||||
return c.service(service, tag, q, true)
|
||||
var tags []string
|
||||
if tag != "" {
|
||||
tags = []string{tag}
|
||||
}
|
||||
return c.service(service, tags, q, true)
|
||||
}
|
||||
|
||||
func (c *Catalog) service(service, tag string, q *QueryOptions, connect bool) ([]*CatalogService, *QueryMeta, error) {
|
||||
// Supports multiple tags for filtering
|
||||
func (c *Catalog) ConnectMultipleTags(service string, tags []string, q *QueryOptions) ([]*CatalogService, *QueryMeta, error) {
|
||||
return c.service(service, tags, q, true)
|
||||
}
|
||||
|
||||
func (c *Catalog) service(service string, tags []string, q *QueryOptions, connect bool) ([]*CatalogService, *QueryMeta, error) {
|
||||
path := "/v1/catalog/service/" + service
|
||||
if connect {
|
||||
path = "/v1/catalog/connect/" + service
|
||||
}
|
||||
r := c.c.newRequest("GET", path)
|
||||
r.setQueryOptions(q)
|
||||
if tag != "" {
|
||||
r.params.Set("tag", tag)
|
||||
if len(tags) > 0 {
|
||||
for _, tag := range tags {
|
||||
r.params.Add("tag", tag)
|
||||
}
|
||||
}
|
||||
rtt, resp, err := requireOK(c.c.doRequest(r))
|
||||
if err != nil {
|
||||
|
|
|
@ -0,0 +1,255 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
const (
|
||||
ServiceDefaults string = "service-defaults"
|
||||
ProxyDefaults string = "proxy-defaults"
|
||||
ProxyConfigGlobal string = "global"
|
||||
)
|
||||
|
||||
type ConfigEntry interface {
|
||||
GetKind() string
|
||||
GetName() string
|
||||
GetCreateIndex() uint64
|
||||
GetModifyIndex() uint64
|
||||
}
|
||||
|
||||
type ServiceConfigEntry struct {
|
||||
Kind string
|
||||
Name string
|
||||
Protocol string
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
func (s *ServiceConfigEntry) GetKind() string {
|
||||
return s.Kind
|
||||
}
|
||||
|
||||
func (s *ServiceConfigEntry) GetName() string {
|
||||
return s.Name
|
||||
}
|
||||
|
||||
func (s *ServiceConfigEntry) GetCreateIndex() uint64 {
|
||||
return s.CreateIndex
|
||||
}
|
||||
|
||||
func (s *ServiceConfigEntry) GetModifyIndex() uint64 {
|
||||
return s.ModifyIndex
|
||||
}
|
||||
|
||||
type ProxyConfigEntry struct {
|
||||
Kind string
|
||||
Name string
|
||||
Config map[string]interface{}
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
func (p *ProxyConfigEntry) GetKind() string {
|
||||
return p.Kind
|
||||
}
|
||||
|
||||
func (p *ProxyConfigEntry) GetName() string {
|
||||
return p.Name
|
||||
}
|
||||
|
||||
func (p *ProxyConfigEntry) GetCreateIndex() uint64 {
|
||||
return p.CreateIndex
|
||||
}
|
||||
|
||||
func (p *ProxyConfigEntry) GetModifyIndex() uint64 {
|
||||
return p.ModifyIndex
|
||||
}
|
||||
|
||||
type rawEntryListResponse struct {
|
||||
kind string
|
||||
Entries []map[string]interface{}
|
||||
}
|
||||
|
||||
func makeConfigEntry(kind, name string) (ConfigEntry, error) {
|
||||
switch kind {
|
||||
case ServiceDefaults:
|
||||
return &ServiceConfigEntry{Name: name}, nil
|
||||
case ProxyDefaults:
|
||||
return &ProxyConfigEntry{Name: name}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid config entry kind: %s", kind)
|
||||
}
|
||||
}
|
||||
|
||||
func DecodeConfigEntry(raw map[string]interface{}) (ConfigEntry, error) {
|
||||
var entry ConfigEntry
|
||||
|
||||
kindVal, ok := raw["Kind"]
|
||||
if !ok {
|
||||
kindVal, ok = raw["kind"]
|
||||
}
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Payload does not contain a kind/Kind key at the top level")
|
||||
}
|
||||
|
||||
if kindStr, ok := kindVal.(string); ok {
|
||||
newEntry, err := makeConfigEntry(kindStr, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry = newEntry
|
||||
} else {
|
||||
return nil, fmt.Errorf("Kind value in payload is not a string")
|
||||
}
|
||||
|
||||
decodeConf := &mapstructure.DecoderConfig{
|
||||
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
|
||||
Result: &entry,
|
||||
WeaklyTypedInput: true,
|
||||
}
|
||||
|
||||
decoder, err := mapstructure.NewDecoder(decodeConf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return entry, decoder.Decode(raw)
|
||||
}
|
||||
|
||||
func DecodeConfigEntryFromJSON(data []byte) (ConfigEntry, error) {
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return DecodeConfigEntry(raw)
|
||||
}
|
||||
|
||||
// Config can be used to query the Config endpoints
|
||||
type ConfigEntries struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// Config returns a handle to the Config endpoints
|
||||
func (c *Client) ConfigEntries() *ConfigEntries {
|
||||
return &ConfigEntries{c}
|
||||
}
|
||||
|
||||
func (conf *ConfigEntries) Get(kind string, name string, q *QueryOptions) (ConfigEntry, *QueryMeta, error) {
|
||||
if kind == "" || name == "" {
|
||||
return nil, nil, fmt.Errorf("Both kind and name parameters must not be empty")
|
||||
}
|
||||
|
||||
entry, err := makeConfigEntry(kind, name)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
r := conf.c.newRequest("GET", fmt.Sprintf("/v1/config/%s/%s", kind, name))
|
||||
r.setQueryOptions(q)
|
||||
rtt, resp, err := requireOK(conf.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
if err := decodeBody(resp, entry); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return entry, qm, nil
|
||||
}
|
||||
|
||||
func (conf *ConfigEntries) List(kind string, q *QueryOptions) ([]ConfigEntry, *QueryMeta, error) {
|
||||
if kind == "" {
|
||||
return nil, nil, fmt.Errorf("The kind parameter must not be empty")
|
||||
}
|
||||
|
||||
r := conf.c.newRequest("GET", fmt.Sprintf("/v1/config/%s", kind))
|
||||
r.setQueryOptions(q)
|
||||
rtt, resp, err := requireOK(conf.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
var raw []map[string]interface{}
|
||||
if err := decodeBody(resp, &raw); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var entries []ConfigEntry
|
||||
for _, rawEntry := range raw {
|
||||
entry, err := DecodeConfigEntry(rawEntry)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
|
||||
return entries, qm, nil
|
||||
}
|
||||
|
||||
func (conf *ConfigEntries) Set(entry ConfigEntry, w *WriteOptions) (bool, *WriteMeta, error) {
|
||||
return conf.set(entry, nil, w)
|
||||
}
|
||||
|
||||
func (conf *ConfigEntries) CAS(entry ConfigEntry, index uint64, w *WriteOptions) (bool, *WriteMeta, error) {
|
||||
return conf.set(entry, map[string]string{"cas": strconv.FormatUint(index, 10)}, w)
|
||||
}
|
||||
|
||||
func (conf *ConfigEntries) set(entry ConfigEntry, params map[string]string, w *WriteOptions) (bool, *WriteMeta, error) {
|
||||
r := conf.c.newRequest("PUT", "/v1/config")
|
||||
r.setWriteOptions(w)
|
||||
for param, value := range params {
|
||||
r.params.Set(param, value)
|
||||
}
|
||||
r.obj = entry
|
||||
rtt, resp, err := requireOK(conf.c.doRequest(r))
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.Copy(&buf, resp.Body); err != nil {
|
||||
return false, nil, fmt.Errorf("Failed to read response: %v", err)
|
||||
}
|
||||
res := strings.Contains(buf.String(), "true")
|
||||
|
||||
wm := &WriteMeta{RequestTime: rtt}
|
||||
return res, wm, nil
|
||||
}
|
||||
|
||||
func (conf *ConfigEntries) Delete(kind string, name string, w *WriteOptions) (*WriteMeta, error) {
|
||||
if kind == "" || name == "" {
|
||||
return nil, fmt.Errorf("Both kind and name parameters must not be empty")
|
||||
}
|
||||
|
||||
r := conf.c.newRequest("DELETE", fmt.Sprintf("/v1/config/%s/%s", kind, name))
|
||||
r.setWriteOptions(w)
|
||||
rtt, resp, err := requireOK(conf.c.doRequest(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Body.Close()
|
||||
wm := &WriteMeta{RequestTime: rtt}
|
||||
return wm, nil
|
||||
}
|
|
@ -21,8 +21,18 @@ type CAConfig struct {
|
|||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
// CommonCAProviderConfig is the common options available to all CA providers.
|
||||
type CommonCAProviderConfig struct {
|
||||
LeafCertTTL time.Duration
|
||||
SkipValidate bool
|
||||
CSRMaxPerSecond float32
|
||||
CSRMaxConcurrent int
|
||||
}
|
||||
|
||||
// ConsulCAProviderConfig is the config for the built-in Consul CA provider.
|
||||
type ConsulCAProviderConfig struct {
|
||||
CommonCAProviderConfig `mapstructure:",squash"`
|
||||
|
||||
PrivateKey string
|
||||
RootCert string
|
||||
RotationPeriod time.Duration
|
||||
|
@ -34,7 +44,6 @@ func ParseConsulCAConfig(raw map[string]interface{}) (*ConsulCAProviderConfig, e
|
|||
var config ConsulCAProviderConfig
|
||||
decodeConf := &mapstructure.DecoderConfig{
|
||||
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
|
||||
ErrorUnused: true,
|
||||
Result: &config,
|
||||
WeaklyTypedInput: true,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Debug can be used to query the /debug/pprof endpoints to gather
|
||||
// profiling information about the target agent.Debug
|
||||
//
|
||||
// The agent must have enable_debug set to true for profiling to be enabled
|
||||
// and for these endpoints to function.
|
||||
type Debug struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// Debug returns a handle that exposes the internal debug endpoints.
|
||||
func (c *Client) Debug() *Debug {
|
||||
return &Debug{c}
|
||||
}
|
||||
|
||||
// Heap returns a pprof heap dump
|
||||
func (d *Debug) Heap() ([]byte, error) {
|
||||
r := d.c.newRequest("GET", "/debug/pprof/heap")
|
||||
_, resp, err := d.c.doRequest(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making request: %s", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// We return a raw response because we're just passing through a response
|
||||
// from the pprof handlers
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding body: %s", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// Profile returns a pprof CPU profile for the specified number of seconds
|
||||
func (d *Debug) Profile(seconds int) ([]byte, error) {
|
||||
r := d.c.newRequest("GET", "/debug/pprof/profile")
|
||||
|
||||
// Capture a profile for the specified number of seconds
|
||||
r.params.Set("seconds", strconv.Itoa(seconds))
|
||||
|
||||
_, resp, err := d.c.doRequest(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making request: %s", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// We return a raw response because we're just passing through a response
|
||||
// from the pprof handlers
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding body: %s", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// Trace returns an execution trace
|
||||
func (d *Debug) Trace(seconds int) ([]byte, error) {
|
||||
r := d.c.newRequest("GET", "/debug/pprof/trace")
|
||||
|
||||
// Capture a trace for the specified number of seconds
|
||||
r.params.Set("seconds", strconv.Itoa(seconds))
|
||||
|
||||
_, resp, err := d.c.doRequest(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making request: %s", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// We return a raw response because we're just passing through a response
|
||||
// from the pprof handlers
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding body: %s", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// Goroutine returns a pprof goroutine profile
|
||||
func (d *Debug) Goroutine() ([]byte, error) {
|
||||
r := d.c.newRequest("GET", "/debug/pprof/goroutine")
|
||||
|
||||
_, resp, err := d.c.doRequest(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making request: %s", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// We return a raw response because we're just passing through a response
|
||||
// from the pprof handlers
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding body: %s", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -36,6 +38,9 @@ type HealthCheck struct {
|
|||
ServiceTags []string
|
||||
|
||||
Definition HealthCheckDefinition
|
||||
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
// HealthCheckDefinition is used to store the details about
|
||||
|
@ -46,11 +51,86 @@ type HealthCheckDefinition struct {
|
|||
Method string
|
||||
TLSSkipVerify bool
|
||||
TCP string
|
||||
IntervalDuration time.Duration `json:"-"`
|
||||
TimeoutDuration time.Duration `json:"-"`
|
||||
DeregisterCriticalServiceAfterDuration time.Duration `json:"-"`
|
||||
|
||||
// DEPRECATED in Consul 1.4.1. Use the above time.Duration fields instead.
|
||||
Interval ReadableDuration
|
||||
Timeout ReadableDuration
|
||||
DeregisterCriticalServiceAfter ReadableDuration
|
||||
}
|
||||
|
||||
func (d *HealthCheckDefinition) MarshalJSON() ([]byte, error) {
|
||||
type Alias HealthCheckDefinition
|
||||
out := &struct {
|
||||
Interval string
|
||||
Timeout string
|
||||
DeregisterCriticalServiceAfter string
|
||||
*Alias
|
||||
}{
|
||||
Interval: d.Interval.String(),
|
||||
Timeout: d.Timeout.String(),
|
||||
DeregisterCriticalServiceAfter: d.DeregisterCriticalServiceAfter.String(),
|
||||
Alias: (*Alias)(d),
|
||||
}
|
||||
|
||||
if d.IntervalDuration != 0 {
|
||||
out.Interval = d.IntervalDuration.String()
|
||||
} else if d.Interval != 0 {
|
||||
out.Interval = d.Interval.String()
|
||||
}
|
||||
if d.TimeoutDuration != 0 {
|
||||
out.Timeout = d.TimeoutDuration.String()
|
||||
} else if d.Timeout != 0 {
|
||||
out.Timeout = d.Timeout.String()
|
||||
}
|
||||
if d.DeregisterCriticalServiceAfterDuration != 0 {
|
||||
out.DeregisterCriticalServiceAfter = d.DeregisterCriticalServiceAfterDuration.String()
|
||||
} else if d.DeregisterCriticalServiceAfter != 0 {
|
||||
out.DeregisterCriticalServiceAfter = d.DeregisterCriticalServiceAfter.String()
|
||||
}
|
||||
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
func (d *HealthCheckDefinition) UnmarshalJSON(data []byte) error {
|
||||
type Alias HealthCheckDefinition
|
||||
aux := &struct {
|
||||
Interval string
|
||||
Timeout string
|
||||
DeregisterCriticalServiceAfter string
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(d),
|
||||
}
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse the values into both the time.Duration and old ReadableDuration fields.
|
||||
var err error
|
||||
if aux.Interval != "" {
|
||||
if d.IntervalDuration, err = time.ParseDuration(aux.Interval); err != nil {
|
||||
return err
|
||||
}
|
||||
d.Interval = ReadableDuration(d.IntervalDuration)
|
||||
}
|
||||
if aux.Timeout != "" {
|
||||
if d.TimeoutDuration, err = time.ParseDuration(aux.Timeout); err != nil {
|
||||
return err
|
||||
}
|
||||
d.Timeout = ReadableDuration(d.TimeoutDuration)
|
||||
}
|
||||
if aux.DeregisterCriticalServiceAfter != "" {
|
||||
if d.DeregisterCriticalServiceAfterDuration, err = time.ParseDuration(aux.DeregisterCriticalServiceAfter); err != nil {
|
||||
return err
|
||||
}
|
||||
d.DeregisterCriticalServiceAfter = ReadableDuration(d.DeregisterCriticalServiceAfterDuration)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HealthChecks is a collection of HealthCheck structs.
|
||||
type HealthChecks []*HealthCheck
|
||||
|
||||
|
@ -159,7 +239,15 @@ func (h *Health) Checks(service string, q *QueryOptions) (HealthChecks, *QueryMe
|
|||
// for a given service. It can optionally do server-side filtering on a tag
|
||||
// or nodes with passing health checks only.
|
||||
func (h *Health) Service(service, tag string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
|
||||
return h.service(service, tag, passingOnly, q, false)
|
||||
var tags []string
|
||||
if tag != "" {
|
||||
tags = []string{tag}
|
||||
}
|
||||
return h.service(service, tags, passingOnly, q, false)
|
||||
}
|
||||
|
||||
func (h *Health) ServiceMultipleTags(service string, tags []string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
|
||||
return h.service(service, tags, passingOnly, q, false)
|
||||
}
|
||||
|
||||
// Connect is equivalent to Service except that it will only return services
|
||||
|
@ -168,18 +256,28 @@ func (h *Health) Service(service, tag string, passingOnly bool, q *QueryOptions)
|
|||
// passingOnly is true only instances where both the service and any proxy are
|
||||
// healthy will be returned.
|
||||
func (h *Health) Connect(service, tag string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
|
||||
return h.service(service, tag, passingOnly, q, true)
|
||||
var tags []string
|
||||
if tag != "" {
|
||||
tags = []string{tag}
|
||||
}
|
||||
return h.service(service, tags, passingOnly, q, true)
|
||||
}
|
||||
|
||||
func (h *Health) service(service, tag string, passingOnly bool, q *QueryOptions, connect bool) ([]*ServiceEntry, *QueryMeta, error) {
|
||||
func (h *Health) ConnectMultipleTags(service string, tags []string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
|
||||
return h.service(service, tags, passingOnly, q, true)
|
||||
}
|
||||
|
||||
func (h *Health) service(service string, tags []string, passingOnly bool, q *QueryOptions, connect bool) ([]*ServiceEntry, *QueryMeta, error) {
|
||||
path := "/v1/health/service/" + service
|
||||
if connect {
|
||||
path = "/v1/health/connect/" + service
|
||||
}
|
||||
r := h.c.newRequest("GET", path)
|
||||
r.setQueryOptions(q)
|
||||
if tag != "" {
|
||||
r.params.Set("tag", tag)
|
||||
if len(tags) > 0 {
|
||||
for _, tag := range tags {
|
||||
r.params.Add("tag", tag)
|
||||
}
|
||||
}
|
||||
if passingOnly {
|
||||
r.params.Set(HealthPassing, "1")
|
||||
|
|
|
@ -45,44 +45,6 @@ type KVPair struct {
|
|||
// KVPairs is a list of KVPair objects
|
||||
type KVPairs []*KVPair
|
||||
|
||||
// KVOp constants give possible operations available in a KVTxn.
|
||||
type KVOp string
|
||||
|
||||
const (
|
||||
KVSet KVOp = "set"
|
||||
KVDelete KVOp = "delete"
|
||||
KVDeleteCAS KVOp = "delete-cas"
|
||||
KVDeleteTree KVOp = "delete-tree"
|
||||
KVCAS KVOp = "cas"
|
||||
KVLock KVOp = "lock"
|
||||
KVUnlock KVOp = "unlock"
|
||||
KVGet KVOp = "get"
|
||||
KVGetTree KVOp = "get-tree"
|
||||
KVCheckSession KVOp = "check-session"
|
||||
KVCheckIndex KVOp = "check-index"
|
||||
KVCheckNotExists KVOp = "check-not-exists"
|
||||
)
|
||||
|
||||
// KVTxnOp defines a single operation inside a transaction.
|
||||
type KVTxnOp struct {
|
||||
Verb KVOp
|
||||
Key string
|
||||
Value []byte
|
||||
Flags uint64
|
||||
Index uint64
|
||||
Session string
|
||||
}
|
||||
|
||||
// KVTxnOps defines a set of operations to be performed inside a single
|
||||
// transaction.
|
||||
type KVTxnOps []*KVTxnOp
|
||||
|
||||
// KVTxnResponse has the outcome of a transaction.
|
||||
type KVTxnResponse struct {
|
||||
Results []*KVPair
|
||||
Errors TxnErrors
|
||||
}
|
||||
|
||||
// KV is used to manipulate the K/V API
|
||||
type KV struct {
|
||||
c *Client
|
||||
|
@ -300,107 +262,18 @@ func (k *KV) deleteInternal(key string, params map[string]string, q *WriteOption
|
|||
return res, qm, nil
|
||||
}
|
||||
|
||||
// TxnOp is the internal format we send to Consul. It's not specific to KV,
|
||||
// though currently only KV operations are supported.
|
||||
type TxnOp struct {
|
||||
KV *KVTxnOp
|
||||
}
|
||||
|
||||
// TxnOps is a list of transaction operations.
|
||||
type TxnOps []*TxnOp
|
||||
|
||||
// TxnResult is the internal format we receive from Consul.
|
||||
type TxnResult struct {
|
||||
KV *KVPair
|
||||
}
|
||||
|
||||
// TxnResults is a list of TxnResult objects.
|
||||
type TxnResults []*TxnResult
|
||||
|
||||
// TxnError is used to return information about an operation in a transaction.
|
||||
type TxnError struct {
|
||||
OpIndex int
|
||||
What string
|
||||
}
|
||||
|
||||
// TxnErrors is a list of TxnError objects.
|
||||
type TxnErrors []*TxnError
|
||||
|
||||
// TxnResponse is the internal format we receive from Consul.
|
||||
type TxnResponse struct {
|
||||
Results TxnResults
|
||||
Errors TxnErrors
|
||||
}
|
||||
|
||||
// Txn is used to apply multiple KV operations in a single, atomic transaction.
|
||||
//
|
||||
// Note that Go will perform the required base64 encoding on the values
|
||||
// automatically because the type is a byte slice. Transactions are defined as a
|
||||
// list of operations to perform, using the KVOp constants and KVTxnOp structure
|
||||
// to define operations. If any operation fails, none of the changes are applied
|
||||
// to the state store. Note that this hides the internal raw transaction interface
|
||||
// and munges the input and output types into KV-specific ones for ease of use.
|
||||
// If there are more non-KV operations in the future we may break out a new
|
||||
// transaction API client, but it will be easy to keep this KV-specific variant
|
||||
// supported.
|
||||
//
|
||||
// Even though this is generally a write operation, we take a QueryOptions input
|
||||
// and return a QueryMeta output. If the transaction contains only read ops, then
|
||||
// Consul will fast-path it to a different endpoint internally which supports
|
||||
// consistency controls, but not blocking. If there are write operations then
|
||||
// the request will always be routed through raft and any consistency settings
|
||||
// will be ignored.
|
||||
//
|
||||
// Here's an example:
|
||||
//
|
||||
// ops := KVTxnOps{
|
||||
// &KVTxnOp{
|
||||
// Verb: KVLock,
|
||||
// Key: "test/lock",
|
||||
// Session: "adf4238a-882b-9ddc-4a9d-5b6758e4159e",
|
||||
// Value: []byte("hello"),
|
||||
// },
|
||||
// &KVTxnOp{
|
||||
// Verb: KVGet,
|
||||
// Key: "another/key",
|
||||
// },
|
||||
// }
|
||||
// ok, response, _, err := kv.Txn(&ops, nil)
|
||||
//
|
||||
// If there is a problem making the transaction request then an error will be
|
||||
// returned. Otherwise, the ok value will be true if the transaction succeeded
|
||||
// or false if it was rolled back. The response is a structured return value which
|
||||
// will have the outcome of the transaction. Its Results member will have entries
|
||||
// for each operation. Deleted keys will have a nil entry in the, and to save
|
||||
// space, the Value of each key in the Results will be nil unless the operation
|
||||
// is a KVGet. If the transaction was rolled back, the Errors member will have
|
||||
// entries referencing the index of the operation that failed along with an error
|
||||
// message.
|
||||
// The Txn function has been deprecated from the KV object; please see the Txn
|
||||
// object for more information about Transactions.
|
||||
func (k *KV) Txn(txn KVTxnOps, q *QueryOptions) (bool, *KVTxnResponse, *QueryMeta, error) {
|
||||
r := k.c.newRequest("PUT", "/v1/txn")
|
||||
r.setQueryOptions(q)
|
||||
|
||||
// Convert into the internal format since this is an all-KV txn.
|
||||
ops := make(TxnOps, 0, len(txn))
|
||||
for _, kvOp := range txn {
|
||||
ops = append(ops, &TxnOp{KV: kvOp})
|
||||
var ops TxnOps
|
||||
for _, op := range txn {
|
||||
ops = append(ops, &TxnOp{KV: op})
|
||||
}
|
||||
r.obj = ops
|
||||
rtt, resp, err := k.c.doRequest(r)
|
||||
|
||||
respOk, txnResp, qm, err := k.c.txn(ops, q)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusConflict {
|
||||
var txnResp TxnResponse
|
||||
if err := decodeBody(resp, &txnResp); err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
|
||||
// Convert from the internal format.
|
||||
kvResp := KVTxnResponse{
|
||||
|
@ -409,12 +282,5 @@ func (k *KV) Txn(txn KVTxnOps, q *QueryOptions) (bool, *KVTxnResponse, *QueryMet
|
|||
for _, result := range txnResp.Results {
|
||||
kvResp.Results = append(kvResp.Results, result.KV)
|
||||
}
|
||||
return resp.StatusCode == http.StatusOK, &kvResp, qm, nil
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.Copy(&buf, resp.Body); err != nil {
|
||||
return false, nil, nil, fmt.Errorf("Failed to read response: %v", err)
|
||||
}
|
||||
return false, nil, nil, fmt.Errorf("Failed request: %s", buf.String())
|
||||
return respOk, &kvResp, qm, nil
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
package api
|
||||
|
||||
// The /v1/operator/area endpoints are available only in Consul Enterprise and
|
||||
// interact with its network area subsystem. Network areas are used to link
|
||||
// together Consul servers in different Consul datacenters. With network areas,
|
||||
// Consul datacenters can be linked together in ways other than a fully-connected
|
||||
// mesh, as is required for Consul's WAN.
|
||||
package api
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
|
|
@ -16,6 +16,9 @@ type KeyringResponse struct {
|
|||
// Segment has the network segment this request corresponds to.
|
||||
Segment string
|
||||
|
||||
// Messages has information or errors from serf
|
||||
Messages map[string]string `json:",omitempty"`
|
||||
|
||||
// A map of the encryption keys to the number of nodes they're installed on
|
||||
Keys map[string]int
|
||||
|
||||
|
|
|
@ -55,6 +55,11 @@ type ServiceQuery struct {
|
|||
// service entry to be returned.
|
||||
NodeMeta map[string]string
|
||||
|
||||
// ServiceMeta is a map of required service metadata fields. If a key/value
|
||||
// pair is in this map it must be present on the node in order for the
|
||||
// service entry to be returned.
|
||||
ServiceMeta map[string]string
|
||||
|
||||
// Connect if true will filter the prepared query results to only
|
||||
// include Connect-capable services. These include both native services
|
||||
// and proxies for matching services. Note that if a proxy matches,
|
||||
|
|
|
@ -0,0 +1,230 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Txn is used to manipulate the Txn API
|
||||
type Txn struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// Txn is used to return a handle to the K/V apis
|
||||
func (c *Client) Txn() *Txn {
|
||||
return &Txn{c}
|
||||
}
|
||||
|
||||
// TxnOp is the internal format we send to Consul. Currently only K/V and
|
||||
// check operations are supported.
|
||||
type TxnOp struct {
|
||||
KV *KVTxnOp
|
||||
Node *NodeTxnOp
|
||||
Service *ServiceTxnOp
|
||||
Check *CheckTxnOp
|
||||
}
|
||||
|
||||
// TxnOps is a list of transaction operations.
|
||||
type TxnOps []*TxnOp
|
||||
|
||||
// TxnResult is the internal format we receive from Consul.
|
||||
type TxnResult struct {
|
||||
KV *KVPair
|
||||
Node *Node
|
||||
Service *CatalogService
|
||||
Check *HealthCheck
|
||||
}
|
||||
|
||||
// TxnResults is a list of TxnResult objects.
|
||||
type TxnResults []*TxnResult
|
||||
|
||||
// TxnError is used to return information about an operation in a transaction.
|
||||
type TxnError struct {
|
||||
OpIndex int
|
||||
What string
|
||||
}
|
||||
|
||||
// TxnErrors is a list of TxnError objects.
|
||||
type TxnErrors []*TxnError
|
||||
|
||||
// TxnResponse is the internal format we receive from Consul.
|
||||
type TxnResponse struct {
|
||||
Results TxnResults
|
||||
Errors TxnErrors
|
||||
}
|
||||
|
||||
// KVOp constants give possible operations available in a transaction.
|
||||
type KVOp string
|
||||
|
||||
const (
|
||||
KVSet KVOp = "set"
|
||||
KVDelete KVOp = "delete"
|
||||
KVDeleteCAS KVOp = "delete-cas"
|
||||
KVDeleteTree KVOp = "delete-tree"
|
||||
KVCAS KVOp = "cas"
|
||||
KVLock KVOp = "lock"
|
||||
KVUnlock KVOp = "unlock"
|
||||
KVGet KVOp = "get"
|
||||
KVGetTree KVOp = "get-tree"
|
||||
KVCheckSession KVOp = "check-session"
|
||||
KVCheckIndex KVOp = "check-index"
|
||||
KVCheckNotExists KVOp = "check-not-exists"
|
||||
)
|
||||
|
||||
// KVTxnOp defines a single operation inside a transaction.
|
||||
type KVTxnOp struct {
|
||||
Verb KVOp
|
||||
Key string
|
||||
Value []byte
|
||||
Flags uint64
|
||||
Index uint64
|
||||
Session string
|
||||
}
|
||||
|
||||
// KVTxnOps defines a set of operations to be performed inside a single
|
||||
// transaction.
|
||||
type KVTxnOps []*KVTxnOp
|
||||
|
||||
// KVTxnResponse has the outcome of a transaction.
|
||||
type KVTxnResponse struct {
|
||||
Results []*KVPair
|
||||
Errors TxnErrors
|
||||
}
|
||||
|
||||
// NodeOp constants give possible operations available in a transaction.
|
||||
type NodeOp string
|
||||
|
||||
const (
|
||||
NodeGet NodeOp = "get"
|
||||
NodeSet NodeOp = "set"
|
||||
NodeCAS NodeOp = "cas"
|
||||
NodeDelete NodeOp = "delete"
|
||||
NodeDeleteCAS NodeOp = "delete-cas"
|
||||
)
|
||||
|
||||
// NodeTxnOp defines a single operation inside a transaction.
|
||||
type NodeTxnOp struct {
|
||||
Verb NodeOp
|
||||
Node Node
|
||||
}
|
||||
|
||||
// ServiceOp constants give possible operations available in a transaction.
|
||||
type ServiceOp string
|
||||
|
||||
const (
|
||||
ServiceGet ServiceOp = "get"
|
||||
ServiceSet ServiceOp = "set"
|
||||
ServiceCAS ServiceOp = "cas"
|
||||
ServiceDelete ServiceOp = "delete"
|
||||
ServiceDeleteCAS ServiceOp = "delete-cas"
|
||||
)
|
||||
|
||||
// ServiceTxnOp defines a single operation inside a transaction.
|
||||
type ServiceTxnOp struct {
|
||||
Verb ServiceOp
|
||||
Node string
|
||||
Service AgentService
|
||||
}
|
||||
|
||||
// CheckOp constants give possible operations available in a transaction.
|
||||
type CheckOp string
|
||||
|
||||
const (
|
||||
CheckGet CheckOp = "get"
|
||||
CheckSet CheckOp = "set"
|
||||
CheckCAS CheckOp = "cas"
|
||||
CheckDelete CheckOp = "delete"
|
||||
CheckDeleteCAS CheckOp = "delete-cas"
|
||||
)
|
||||
|
||||
// CheckTxnOp defines a single operation inside a transaction.
|
||||
type CheckTxnOp struct {
|
||||
Verb CheckOp
|
||||
Check HealthCheck
|
||||
}
|
||||
|
||||
// Txn is used to apply multiple Consul operations in a single, atomic transaction.
|
||||
//
|
||||
// Note that Go will perform the required base64 encoding on the values
|
||||
// automatically because the type is a byte slice. Transactions are defined as a
|
||||
// list of operations to perform, using the different fields in the TxnOp structure
|
||||
// to define operations. If any operation fails, none of the changes are applied
|
||||
// to the state store.
|
||||
//
|
||||
// Even though this is generally a write operation, we take a QueryOptions input
|
||||
// and return a QueryMeta output. If the transaction contains only read ops, then
|
||||
// Consul will fast-path it to a different endpoint internally which supports
|
||||
// consistency controls, but not blocking. If there are write operations then
|
||||
// the request will always be routed through raft and any consistency settings
|
||||
// will be ignored.
|
||||
//
|
||||
// Here's an example:
|
||||
//
|
||||
// ops := KVTxnOps{
|
||||
// &KVTxnOp{
|
||||
// Verb: KVLock,
|
||||
// Key: "test/lock",
|
||||
// Session: "adf4238a-882b-9ddc-4a9d-5b6758e4159e",
|
||||
// Value: []byte("hello"),
|
||||
// },
|
||||
// &KVTxnOp{
|
||||
// Verb: KVGet,
|
||||
// Key: "another/key",
|
||||
// },
|
||||
// &CheckTxnOp{
|
||||
// Verb: CheckSet,
|
||||
// HealthCheck: HealthCheck{
|
||||
// Node: "foo",
|
||||
// CheckID: "redis:a",
|
||||
// Name: "Redis Health Check",
|
||||
// Status: "passing",
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
// ok, response, _, err := kv.Txn(&ops, nil)
|
||||
//
|
||||
// If there is a problem making the transaction request then an error will be
|
||||
// returned. Otherwise, the ok value will be true if the transaction succeeded
|
||||
// or false if it was rolled back. The response is a structured return value which
|
||||
// will have the outcome of the transaction. Its Results member will have entries
|
||||
// for each operation. For KV operations, Deleted keys will have a nil entry in the
|
||||
// results, and to save space, the Value of each key in the Results will be nil
|
||||
// unless the operation is a KVGet. If the transaction was rolled back, the Errors
|
||||
// member will have entries referencing the index of the operation that failed
|
||||
// along with an error message.
|
||||
func (t *Txn) Txn(txn TxnOps, q *QueryOptions) (bool, *TxnResponse, *QueryMeta, error) {
|
||||
return t.c.txn(txn, q)
|
||||
}
|
||||
|
||||
func (c *Client) txn(txn TxnOps, q *QueryOptions) (bool, *TxnResponse, *QueryMeta, error) {
|
||||
r := c.newRequest("PUT", "/v1/txn")
|
||||
r.setQueryOptions(q)
|
||||
|
||||
r.obj = txn
|
||||
rtt, resp, err := c.doRequest(r)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
qm := &QueryMeta{}
|
||||
parseQueryMeta(resp, qm)
|
||||
qm.RequestTime = rtt
|
||||
|
||||
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusConflict {
|
||||
var txnResp TxnResponse
|
||||
if err := decodeBody(resp, &txnResp); err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
|
||||
return resp.StatusCode == http.StatusOK, &txnResp, qm, nil
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.Copy(&buf, resp.Body); err != nil {
|
||||
return false, nil, nil, fmt.Errorf("Failed to read response: %v", err)
|
||||
}
|
||||
return false, nil, nil, fmt.Errorf("Failed request: %s", buf.String())
|
||||
}
|
|
@ -63,7 +63,13 @@
|
|||
{"path":"github.com/containerd/console","checksumSHA1":"IGtuR58l2zmYRcNf8sPDlCSgovE=","origin":"github.com/opencontainers/runc/vendor/github.com/containerd/console","revision":"459bfaec1fc6c17d8bfb12d0a0f69e7e7271ed2a","revisionTime":"2018-08-23T14:46:37Z"},
|
||||
{"path":"github.com/containerd/continuity/pathdriver","checksumSHA1":"GqIrOttKaO7k6HIaHQLPr3cY7rY=","origin":"github.com/docker/docker/vendor/github.com/containerd/continuity/pathdriver","revision":"320063a2ad06a1d8ada61c94c29dbe44e2d87473","revisionTime":"2018-08-16T08:14:46Z"},
|
||||
{"path":"github.com/containerd/fifo","checksumSHA1":"Ur3lVmFp+HTGUzQU+/ZBolKe8FU=","revision":"3d5202aec260678c48179c56f40e6f38a095738c","revisionTime":"2018-03-07T16:51:37Z"},
|
||||
{"path":"github.com/containernetworking/cni/pkg/types","checksumSHA1":"NeAp/3+Hedu9tnMai+LihERPj84=","revision":"5c3c17164270150467498a32c71436c7cd5501be","revisionTime":"2016-06-02T16:00:07Z"},
|
||||
{"path":"github.com/containernetworking/cni/libcni","checksumSHA1":"3CsFN6YsShG9EU2oB9vJIqYTxq4=","revision":"dc953e2fd91f9bc624b03cf9ea3706796bfee920","revisionTime":"2019-06-12T15:24:20Z"},
|
||||
{"path":"github.com/containernetworking/cni/pkg/invoke","checksumSHA1":"Xf2DxXUyjBO9u4LeyDzS38pdL+I=","revision":"dc953e2fd91f9bc624b03cf9ea3706796bfee920","revisionTime":"2019-06-12T15:24:20Z"},
|
||||
{"path":"github.com/containernetworking/cni/pkg/types","checksumSHA1":"Dhi4+8X7U2oVzVkgxPrmLaN8qFI=","revision":"dc953e2fd91f9bc624b03cf9ea3706796bfee920","revisionTime":"2019-06-12T15:24:20Z"},
|
||||
{"path":"github.com/containernetworking/cni/pkg/types/020","checksumSHA1":"6+ng8oaM9SB0TyCE7I7N940IpPY=","revision":"dc953e2fd91f9bc624b03cf9ea3706796bfee920","revisionTime":"2019-06-12T15:24:20Z"},
|
||||
{"path":"github.com/containernetworking/cni/pkg/types/current","checksumSHA1":"X6dNZ3yc3V9ffW9vz4yIyeKXGD0=","revision":"dc953e2fd91f9bc624b03cf9ea3706796bfee920","revisionTime":"2019-06-12T15:24:20Z"},
|
||||
{"path":"github.com/containernetworking/cni/pkg/version","checksumSHA1":"aojwjPoA9XSGB/zVtOGzWvmv/i8=","revision":"dc953e2fd91f9bc624b03cf9ea3706796bfee920","revisionTime":"2019-06-12T15:24:20Z"},
|
||||
{"path":"github.com/containernetworking/plugins/pkg/ns","checksumSHA1":"n3dCDigZOU+eD84Cr4Kg30GO4nI=","revision":"2d6d46d308b2c45a0466324c9e3f1330ab5cacd6","revisionTime":"2019-05-01T19:17:48Z"},
|
||||
{"path":"github.com/coreos/go-semver/semver","checksumSHA1":"97BsbXOiZ8+Kr+LIuZkQFtSj7H4=","revision":"1817cd4bea52af76542157eeabd74b057d1a199e","revisionTime":"2017-06-13T09:22:38Z"},
|
||||
{"path":"github.com/coreos/go-systemd/dbus","checksumSHA1":"/zxxFPYjUB7Wowz33r5AhTDvoz0=","origin":"github.com/opencontainers/runc/vendor/github.com/coreos/go-systemd/dbus","revision":"459bfaec1fc6c17d8bfb12d0a0f69e7e7271ed2a","revisionTime":"2018-08-23T14:46:37Z"},
|
||||
{"path":"github.com/coreos/go-systemd/util","checksumSHA1":"e8qgBHxXbij3RVspqrkeBzMZ564=","origin":"github.com/opencontainers/runc/vendor/github.com/coreos/go-systemd/util","revision":"459bfaec1fc6c17d8bfb12d0a0f69e7e7271ed2a","revisionTime":"2018-08-23T14:46:37Z"},
|
||||
|
@ -169,7 +175,7 @@
|
|||
{"path":"github.com/hashicorp/consul-template/version","checksumSHA1":"85qK+LAbb/oAjvdDqVOLi4tMxZk=","revision":"4058b146979c4feb0551d39b8795a31409b3e6bf","revisionTime":"2019-07-17T18:51:08Z"},
|
||||
{"path":"github.com/hashicorp/consul-template/watch","checksumSHA1":"cJxopvJKg7DBBb8tnDsfmBp5Q8I=","revision":"4058b146979c4feb0551d39b8795a31409b3e6bf","revisionTime":"2019-07-17T18:51:08Z"},
|
||||
{"path":"github.com/hashicorp/consul/agent/consul/autopilot","checksumSHA1":"+I7fgoQlrnTUGW5krqNLadWwtjg=","revision":"fb848fc48818f58690db09d14640513aa6bf3c02","revisionTime":"2018-04-13T17:05:42Z"},
|
||||
{"path":"github.com/hashicorp/consul/api","checksumSHA1":"sjEf6EMTPP4NT3m5a0JJmlbLZ8Y=","revision":"39f93f011e591c842acc8053a7f5972aa6e592fd","revisionTime":"2018-07-12T16:33:56Z"},
|
||||
{"path":"github.com/hashicorp/consul/api","checksumSHA1":"7JPBtnIgLkdcJ0ldXMTEnVjNEjA=","revision":"40cec98468b829e5cdaacb0629b3e23a028db688","revisionTime":"2019-05-22T20:19:12Z"},
|
||||
{"path":"github.com/hashicorp/consul/command/flags","checksumSHA1":"soNN4xaHTbeXFgNkZ7cX0gbFXQk=","revision":"fb848fc48818f58690db09d14640513aa6bf3c02","revisionTime":"2018-04-13T17:05:42Z"},
|
||||
{"path":"github.com/hashicorp/consul/lib","checksumSHA1":"Nrh9BhiivRyJiuPzttstmq9xl/w=","revision":"fb848fc48818f58690db09d14640513aa6bf3c02","revisionTime":"2018-04-13T17:05:42Z"},
|
||||
{"path":"github.com/hashicorp/consul/lib/freeport","checksumSHA1":"E28E4zR1FN2v1Xiq4FUER7KVN9M=","revision":"fb848fc48818f58690db09d14640513aa6bf3c02","revisionTime":"2018-04-13T17:05:42Z"},
|
||||
|
|
|
@ -136,6 +136,17 @@ driver) but will be removed in a future release.
|
|||
generated, but setting this to `false` will use the system's UUID. Before
|
||||
Nomad 0.6 the default was to use the system UUID.
|
||||
|
||||
- `cni_path` `(string: "/opt/cni/bin")` - Sets the search path that is used for
|
||||
CNI plugin discovery. Multiple paths can be searched using colon delimited
|
||||
paths
|
||||
|
||||
- `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
|
||||
client.
|
||||
|
||||
- `bridge_network_subnet` `(string: "172.26.66.0/23")` - Specifies the subnet
|
||||
which the client will use to allocate IP addresses from.
|
||||
|
||||
### `chroot_env` Parameters
|
||||
|
||||
Drivers based on [isolated fork/exec](/docs/drivers/exec.html) implement file
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue