From 349bf8f821fbd1d4a0825a4a058753e2cfd65a84 Mon Sep 17 00:00:00 2001 From: ryane Date: Thu, 3 Nov 2016 11:27:57 -0400 Subject: [PATCH 01/14] docker.network: core implementation missing some options --- load/resource.go | 1 + resource/docker/docker.go | 60 +++++ resource/docker/network/network.go | 158 ++++++++++++ resource/docker/network/network_test.go | 313 +++++++++++++++++++++++ resource/docker/network/preparer.go | 78 ++++++ resource/docker/network/preparer_test.go | 53 ++++ samples/dockerNetwork.hcl | 10 + 7 files changed, 673 insertions(+) create mode 100644 resource/docker/network/network.go create mode 100644 resource/docker/network/network_test.go create mode 100644 resource/docker/network/preparer.go create mode 100644 resource/docker/network/preparer_test.go create mode 100644 samples/dockerNetwork.hcl diff --git a/load/resource.go b/load/resource.go index ead430c7b..15e23f8b6 100644 --- a/load/resource.go +++ b/load/resource.go @@ -28,6 +28,7 @@ import ( // import empty to register types for SetResources _ "github.com/asteris-llc/converge/resource/docker/container" _ "github.com/asteris-llc/converge/resource/docker/image" + _ "github.com/asteris-llc/converge/resource/docker/network" _ "github.com/asteris-llc/converge/resource/docker/volume" _ "github.com/asteris-llc/converge/resource/file/content" _ "github.com/asteris-llc/converge/resource/file/directory" diff --git a/resource/docker/docker.go b/resource/docker/docker.go index f83ddacc9..e1fdccc39 100644 --- a/resource/docker/docker.go +++ b/resource/docker/docker.go @@ -45,6 +45,13 @@ type Client struct { PullInactivityTimeout time.Duration } +// NetworkClient manage Docker networks +type NetworkClient interface { + FindNetwork(string) (*dc.Network, error) + CreateNetwork(dc.CreateNetworkOptions) (*dc.Network, error) + RemoveNetwork(string) error +} + // NewDockerClient returns a docker client with the default configuration func NewDockerClient() (*Client, error) { c, err := dc.NewClientFromEnv() @@ -257,3 +264,56 @@ func (c *Client) FindVolume(name string) (*dc.Volume, error) { } return volume, nil } + +// FindNetwork finds the network with the specified name +func (c *Client) FindNetwork(name string) (*dc.Network, error) { + networks, err := c.Client.FilteredListNetworks(dc.NetworkFilterOpts{ + "type": map[string]bool{"custom": true}, + "name": map[string]bool{name: true}, + }) + + if err != nil { + return nil, errors.Wrap(err, "failed to list networks") + } + + for _, nw := range networks { + if strings.EqualFold(nw.Name, name) { + return &nw, nil + } + } + + return nil, nil +} + +// CreateNetwork creates a docker network +func (c *Client) CreateNetwork(opts dc.CreateNetworkOptions) (*dc.Network, error) { + log.WithFields(log.Fields{ + "module": "docker", + "function": "CreateNetwork", + }).Debugf("creating network %s: %+v", opts.Name, opts) + + nw, err := c.Client.CreateNetwork(opts) + if err != nil { + return nil, errors.Wrap(err, "failed to create network") + } + return nw, err +} + +// RemoveNetwork removes a docker volume +func (c *Client) RemoveNetwork(name string) error { + log.WithFields(log.Fields{ + "module": "docker", + "function": "RemoveNetwork", + }).Debugf("removing network %s", name) + + nw, err := c.FindNetwork(name) + if err != nil { + return err + } + + err = c.Client.RemoveNetwork(nw.ID) + if err != nil { + return errors.Wrap(err, "failed to remove network") + } + return nil +} diff --git a/resource/docker/network/network.go b/resource/docker/network/network.go new file mode 100644 index 000000000..0ca42773d --- /dev/null +++ b/resource/docker/network/network.go @@ -0,0 +1,158 @@ +// Copyright © 2016 Asteris, LLC +// +// 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 network + +import ( + "fmt" + "sort" + "strings" + + "github.com/asteris-llc/converge/helpers/transform" + "github.com/asteris-llc/converge/resource" + "github.com/asteris-llc/converge/resource/docker" + dc "github.com/fsouza/go-dockerclient" +) + +// State type for Network +type State string + +const ( + // StatePresent indicates the network should be present + StatePresent State = "present" + + // StateAbsent indicates the network should be absent + StateAbsent State = "absent" + + // DefaultDriver is the default network driver + DefaultDriver = "bridge" +) + +// Network is responsible for managing docker volumes +type Network struct { + *resource.Status + client docker.NetworkClient + + Name string + Driver string + Labels map[string]string + Options map[string]interface{} + State State + Force bool +} + +// Check system for docker network +func (n *Network) Check(resource.Renderer) (resource.TaskStatus, error) { + n.Status = resource.NewStatus() + nw, err := n.client.FindNetwork(n.Name) + + if err != nil { + n.RaiseLevel(resource.StatusFatal) + return n, err + } + + n.AddDifference(n.Name, string(networkState(nw)), string(n.State), "") + + if n.State == StatePresent && nw != nil && n.Force { + n.AddDifference("labels", mapCompareStr(nw.Labels), mapCompareStr(n.Labels), "") + n.AddDifference("driver", nw.Driver, n.Driver, DefaultDriver) + } + + if resource.AnyChanges(n.Differences) { + n.RaiseLevel(resource.StatusWillChange) + } + + return n, nil +} + +// Apply ensures the network matches the desired state +func (n *Network) Apply() (resource.TaskStatus, error) { + n.Status = resource.NewStatus() + + var ( + nw *dc.Network + err error + ) + + nw, err = n.client.FindNetwork(n.Name) + if err != nil { + n.RaiseLevel(resource.StatusFatal) + return n, err + } + + if n.State == StatePresent { + if nw != nil { + if !n.Force { + return n, nil + } + + err = n.client.RemoveNetwork(n.Name) + if err != nil { + n.RaiseLevel(resource.StatusFatal) + return n, err + } + n.AddMessage(fmt.Sprintf("removed network %s", n.Name)) + } + + opts := dc.CreateNetworkOptions{ + Name: n.Name, + Driver: n.Driver, + Labels: n.Labels, + Options: n.Options, + } + nw, err = n.client.CreateNetwork(opts) + if err != nil { + n.RaiseLevel(resource.StatusFatal) + return n, err + } + n.AddMessage(fmt.Sprintf("created network %s", n.Name)) + n.RaiseLevel(resource.StatusWillChange) + } else { + if nw != nil { + err = n.client.RemoveNetwork(n.Name) + if err != nil { + n.RaiseLevel(resource.StatusFatal) + return n, err + } + n.AddMessage(fmt.Sprintf("removed network %s", n.Name)) + n.RaiseLevel(resource.StatusWillChange) + } + } + + n.AddDifference(n.Name, string(networkState(nw)), string(n.State), "") + return n, nil +} + +// SetClient injects a docker api client +func (n *Network) SetClient(client docker.NetworkClient) { + n.client = client +} + +func networkState(nw *dc.Network) State { + if nw != nil { + return StatePresent + } + return StateAbsent +} + +func mapCompareStr(m map[string]string) string { + pairs := transform.StringsMapToStringSlice( + m, + func(k, v string) string { + return fmt.Sprintf("%s=%s", k, v) + }, + ) + sort.Strings(pairs) + return strings.Join(pairs, ", ") +} diff --git a/resource/docker/network/network_test.go b/resource/docker/network/network_test.go new file mode 100644 index 000000000..9b4859bf7 --- /dev/null +++ b/resource/docker/network/network_test.go @@ -0,0 +1,313 @@ +// Copyright © 2016 Asteris, LLC +// +// 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 network_test + +import ( + "errors" + "testing" + + "github.com/asteris-llc/converge/helpers/comparison" + "github.com/asteris-llc/converge/helpers/fakerenderer" + "github.com/asteris-llc/converge/helpers/logging" + "github.com/asteris-llc/converge/resource" + "github.com/asteris-llc/converge/resource/docker/network" + dc "github.com/fsouza/go-dockerclient" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +// TestNetworkInterface verifies that Network implements the resource.Task +// interface +func TestNetworkInterface(t *testing.T) { + t.Parallel() + defer logging.HideLogs(t)() + + assert.Implements(t, (*resource.Task)(nil), new(network.Network)) +} + +// TestNetworkCheck tests the Network.Check function +func TestNetworkCheck(t *testing.T) { + t.Parallel() + defer logging.HideLogs(t)() + + nwName := "test-network" + + t.Run("state: absent", func(t *testing.T) { + t.Run("network does not exist", func(t *testing.T) { + nw := &network.Network{Name: nwName, State: "absent"} + c := &mockClient{} + nw.SetClient(c) + c.On("FindNetwork", nwName).Return(nil, nil) + + status, err := nw.Check(fakerenderer.New()) + require.NoError(t, err) + assert.False(t, status.HasChanges()) + assert.Equal(t, 1, len(status.Diffs())) + comparison.AssertDiff(t, status.Diffs(), nwName, "absent", "absent") + }) + + t.Run("network exists", func(t *testing.T) { + nw := &network.Network{Name: nwName, State: "absent"} + c := &mockClient{} + nw.SetClient(c) + c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName}, nil) + + status, err := nw.Check(fakerenderer.New()) + require.NoError(t, err) + assert.True(t, status.HasChanges()) + assert.Equal(t, 1, len(status.Diffs())) + comparison.AssertDiff(t, status.Diffs(), nwName, "present", "absent") + }) + }) + + t.Run("state: present", func(t *testing.T) { + t.Run("network does not exist", func(t *testing.T) { + nw := &network.Network{Name: nwName, State: "present"} + c := &mockClient{} + nw.SetClient(c) + c.On("FindNetwork", nwName).Return(nil, nil) + + status, err := nw.Check(fakerenderer.New()) + require.NoError(t, err) + assert.True(t, status.HasChanges()) + assert.Equal(t, 1, len(status.Diffs())) + comparison.AssertDiff(t, status.Diffs(), nwName, "absent", "present") + }) + + t.Run("network exists", func(t *testing.T) { + nw := &network.Network{Name: nwName, State: "present"} + c := &mockClient{} + nw.SetClient(c) + c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName}, nil) + + status, err := nw.Check(fakerenderer.New()) + require.NoError(t, err) + assert.False(t, status.HasChanges()) + assert.Equal(t, 1, len(status.Diffs())) + comparison.AssertDiff(t, status.Diffs(), nwName, "present", "present") + }) + + t.Run("network exists, force: true", func(t *testing.T) { + t.Run("labels", func(t *testing.T) { + nw := &network.Network{ + Name: nwName, + State: "present", + Labels: map[string]string{"key": "val", "test": "val2"}, + Force: true, + } + c := &mockClient{} + nw.SetClient(c) + c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName}, nil) + + status, err := nw.Check(fakerenderer.New()) + require.NoError(t, err) + assert.True(t, status.HasChanges()) + assert.True(t, len(status.Diffs()) > 1) + comparison.AssertDiff(t, status.Diffs(), nwName, "present", "present") + comparison.AssertDiff(t, status.Diffs(), "labels", "", "key=val, test=val2") + }) + + t.Run("driver", func(t *testing.T) { + nw := &network.Network{ + Name: nwName, + State: "present", + Driver: "weave", + Force: true, + } + c := &mockClient{} + nw.SetClient(c) + c.On("FindNetwork", nwName).Return(&dc.Network{ + Name: nwName, + Driver: network.DefaultDriver, + }, nil) + + status, err := nw.Check(fakerenderer.New()) + require.NoError(t, err) + assert.True(t, status.HasChanges()) + assert.True(t, len(status.Diffs()) > 1) + comparison.AssertDiff(t, status.Diffs(), nwName, "present", "present") + comparison.AssertDiff(t, status.Diffs(), "driver", network.DefaultDriver, "weave") + }) + }) + }) + + t.Run("docker api error", func(t *testing.T) { + nw := &network.Network{Name: nwName, State: "present"} + c := &mockClient{} + nw.SetClient(c) + c.On("FindNetwork", nwName).Return(nil, errors.New("error")) + + status, err := nw.Check(fakerenderer.New()) + require.Error(t, err) + assert.Equal(t, resource.StatusFatal, status.StatusCode()) + }) +} + +// TestNetworkApply tests the Network.Apply function +func TestNetworkApply(t *testing.T) { + t.Parallel() + defer logging.HideLogs(t)() + + nwName := "test-network" + + t.Run("docker find network error", func(t *testing.T) { + nw := &network.Network{Name: nwName} + c := &mockClient{} + nw.SetClient(c) + c.On("FindNetwork", nwName).Return(nil, errors.New("error")) + + status, err := nw.Apply() + require.Error(t, err) + assert.Equal(t, resource.StatusFatal, status.StatusCode()) + }) + + t.Run("state: present", func(t *testing.T) { + t.Run("network does not exist", func(t *testing.T) { + nw := &network.Network{Name: nwName, State: "present"} + c := &mockClient{} + nw.SetClient(c) + c.On("FindNetwork", nwName).Return(nil, nil) + c.On("CreateNetwork", mock.AnythingOfType("docker.CreateNetworkOptions")). + Return(&dc.Network{Name: nwName}, nil) + + status, err := nw.Apply() + require.NoError(t, err) + assert.True(t, status.HasChanges()) + c.AssertCalled(t, "CreateNetwork", mock.AnythingOfType("docker.CreateNetworkOptions")) + c.AssertNotCalled(t, "RemoveNetwork", nwName) + }) + + t.Run("network exists, force: false", func(t *testing.T) { + nw := &network.Network{Name: nwName, State: "present"} + c := &mockClient{} + nw.SetClient(c) + c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName}, nil) + + status, err := nw.Apply() + require.NoError(t, err) + assert.False(t, status.HasChanges()) + c.AssertNotCalled(t, "CreateNetwork", mock.AnythingOfType("docker.CreateNetworkOptions")) + c.AssertNotCalled(t, "RemoveNetwork", nwName) + }) + + t.Run("network exists, force: true", func(t *testing.T) { + nw := &network.Network{Name: nwName, State: "present", Force: true} + c := &mockClient{} + nw.SetClient(c) + c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName}, nil) + c.On("RemoveNetwork", nwName).Return(nil) + c.On("CreateNetwork", mock.AnythingOfType("docker.CreateNetworkOptions")). + Return(&dc.Network{Name: nwName}, nil) + + status, err := nw.Apply() + require.NoError(t, err) + assert.True(t, status.HasChanges()) + c.AssertCalled(t, "RemoveNetwork", nwName) + c.AssertCalled(t, "CreateNetwork", mock.AnythingOfType("docker.CreateNetworkOptions")) + }) + + t.Run("docker create network error", func(t *testing.T) { + nw := &network.Network{Name: nwName, State: "present"} + c := &mockClient{} + nw.SetClient(c) + c.On("FindNetwork", nwName).Return(nil, nil) + c.On("CreateNetwork", mock.AnythingOfType("docker.CreateNetworkOptions")). + Return(nil, errors.New("error")) + + status, err := nw.Apply() + require.Error(t, err) + assert.Equal(t, resource.StatusFatal, status.StatusCode()) + }) + + t.Run("docker remove network error", func(t *testing.T) { + nw := &network.Network{Name: nwName, State: "present", Force: true} + c := &mockClient{} + nw.SetClient(c) + c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName, Driver: "test"}, nil) + c.On("RemoveNetwork", mock.AnythingOfType("string")).Return(errors.New("error")) + + status, err := nw.Apply() + require.Error(t, err) + assert.Equal(t, resource.StatusFatal, status.StatusCode()) + }) + }) + + t.Run("state: absent", func(t *testing.T) { + t.Run("network exists", func(t *testing.T) { + nw := &network.Network{Name: nwName, State: "absent"} + c := &mockClient{} + nw.SetClient(c) + c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName}, nil) + c.On("RemoveNetwork", nwName).Return(nil) + + status, err := nw.Apply() + require.NoError(t, err) + assert.True(t, status.HasChanges()) + c.AssertCalled(t, "RemoveNetwork", nwName) + }) + + t.Run("network does not exist", func(t *testing.T) { + nw := &network.Network{Name: nwName, State: "absent"} + c := &mockClient{} + nw.SetClient(c) + c.On("FindNetwork", nwName).Return(nil, nil) + + status, err := nw.Apply() + require.NoError(t, err) + assert.False(t, status.HasChanges()) + c.AssertNotCalled(t, "RemoveNetwork", nwName) + }) + + t.Run("docker remove network error", func(t *testing.T) { + nw := &network.Network{Name: nwName, State: "absent"} + c := &mockClient{} + nw.SetClient(c) + c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName}, nil) + c.On("RemoveNetwork", nwName).Return(errors.New("error")) + + status, err := nw.Apply() + require.Error(t, err) + assert.Equal(t, resource.StatusFatal, status.StatusCode()) + }) + }) +} + +type mockClient struct { + mock.Mock +} + +func (m *mockClient) FindNetwork(name string) (*dc.Network, error) { + args := m.Called(name) + ret := args.Get(0) + if ret == nil { + return nil, args.Error(1) + } + return ret.(*dc.Network), args.Error(1) +} + +func (m *mockClient) CreateNetwork(opts dc.CreateNetworkOptions) (*dc.Network, error) { + args := m.Called(opts) + ret := args.Get(0) + if ret == nil { + return nil, args.Error(1) + } + return ret.(*dc.Network), args.Error(1) +} + +func (m *mockClient) RemoveNetwork(name string) error { + args := m.Called(name) + return args.Error(0) +} diff --git a/resource/docker/network/preparer.go b/resource/docker/network/preparer.go new file mode 100644 index 000000000..81c91f198 --- /dev/null +++ b/resource/docker/network/preparer.go @@ -0,0 +1,78 @@ +// Copyright © 2016 Asteris, LLC +// +// 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 network + +import ( + "github.com/asteris-llc/converge/load/registry" + "github.com/asteris-llc/converge/resource" + "github.com/asteris-llc/converge/resource/docker" +) + +// Preparer for docker networks +// +// Network is responsible for managing Docker networks. It assumes that there is +// already a Docker daemon running on the system. +type Preparer struct { + // name of the network + Name string `hcl:"name" required:"true"` + + // network driver. default: bridge + Driver string `hcl:"driver"` + + // labels to set on the network + Labels map[string]string `hcl:"labels"` + + // driver specific options + Options map[string]interface{} `hcl:"options"` + + // indicates whether the volume should exist. + State State `hcl:"state" valid_values:"present,absent"` + + // indicates whether or not the volume will be recreated if the state is not + // what is expected. By default, the module will only check to see if the + // volume exists. Specified as a boolean value + Force bool `hcl:"force"` +} + +// Prepare a docker network +func (p *Preparer) Prepare(render resource.Renderer) (resource.Task, error) { + dockerClient, err := docker.NewDockerClient() + if err != nil { + return nil, err + } + + if p.Driver == "" { + p.Driver = DefaultDriver + } + + if p.State == "" { + p.State = "present" + } + + nw := &Network{ + Name: p.Name, + Driver: p.Driver, + Labels: p.Labels, + Options: p.Options, + State: p.State, + Force: p.Force, + } + nw.SetClient(dockerClient) + return nw, nil +} + +func init() { + registry.Register("docker.network", (*Preparer)(nil), (*Network)(nil)) +} diff --git a/resource/docker/network/preparer_test.go b/resource/docker/network/preparer_test.go new file mode 100644 index 000000000..dd664dea7 --- /dev/null +++ b/resource/docker/network/preparer_test.go @@ -0,0 +1,53 @@ +// Copyright © 2016 Asteris, LLC +// +// 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 network_test + +import ( + "testing" + + "github.com/asteris-llc/converge/helpers/fakerenderer" + "github.com/asteris-llc/converge/resource" + "github.com/asteris-llc/converge/resource/docker/network" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestPreparerInterface ensures that the correct interfaces are implemented by +// the preparer +func TestPreparerInterface(t *testing.T) { + t.Parallel() + assert.Implements(t, (*resource.Resource)(nil), new(network.Preparer)) +} + +// TestPreparerPrepare tests the Prepare function +func TestPreparerPrepare(t *testing.T) { + t.Run("state defaults to present", func(t *testing.T) { + p := &network.Preparer{Name: "test-network"} + task, err := p.Prepare(fakerenderer.New()) + require.NoError(t, err) + require.IsType(t, (*network.Network)(nil), task) + nw := task.(*network.Network) + assert.Equal(t, network.StatePresent, nw.State) + }) + + t.Run("driver defaults to bridge", func(t *testing.T) { + p := &network.Preparer{Name: "test-network"} + task, err := p.Prepare(fakerenderer.New()) + require.NoError(t, err) + require.IsType(t, (*network.Network)(nil), task) + nw := task.(*network.Network) + assert.Equal(t, network.DefaultDriver, nw.Driver) + }) +} diff --git a/samples/dockerNetwork.hcl b/samples/dockerNetwork.hcl new file mode 100644 index 000000000..2ac4cab74 --- /dev/null +++ b/samples/dockerNetwork.hcl @@ -0,0 +1,10 @@ +docker.network "test-network" { + name = "test-network" + state = "present" + + labels { + environment = "test" + } + + force = true +} From a9a607efe76b81995a739932a9f8937d8621a2d1 Mon Sep 17 00:00:00 2001 From: ryane Date: Thu, 3 Nov 2016 13:00:52 -0400 Subject: [PATCH 02/14] docker.network: network options implemented --- resource/docker/network/network.go | 9 +++++++++ resource/docker/network/network_test.go | 23 +++++++++++++++++++++++ samples/dockerNetwork.hcl | 4 ++++ 3 files changed, 36 insertions(+) diff --git a/resource/docker/network/network.go b/resource/docker/network/network.go index 0ca42773d..43e8c7928 100644 --- a/resource/docker/network/network.go +++ b/resource/docker/network/network.go @@ -67,6 +67,7 @@ func (n *Network) Check(resource.Renderer) (resource.TaskStatus, error) { if n.State == StatePresent && nw != nil && n.Force { n.AddDifference("labels", mapCompareStr(nw.Labels), mapCompareStr(n.Labels), "") n.AddDifference("driver", nw.Driver, n.Driver, DefaultDriver) + n.AddDifference("options", mapCompareStr(nw.Options), mapCompareStr(toStrMap(n.Options)), "") } if resource.AnyChanges(n.Differences) { @@ -146,6 +147,14 @@ func networkState(nw *dc.Network) State { return StateAbsent } +func toStrMap(m map[string]interface{}) map[string]string { + strmap := make(map[string]string) + for k, v := range m { + strmap[k] = v.(string) + } + return strmap +} + func mapCompareStr(m map[string]string) string { pairs := transform.StringsMapToStringSlice( m, diff --git a/resource/docker/network/network_test.go b/resource/docker/network/network_test.go index 9b4859bf7..f75e03ef0 100644 --- a/resource/docker/network/network_test.go +++ b/resource/docker/network/network_test.go @@ -141,6 +141,29 @@ func TestNetworkCheck(t *testing.T) { comparison.AssertDiff(t, status.Diffs(), nwName, "present", "present") comparison.AssertDiff(t, status.Diffs(), "driver", network.DefaultDriver, "weave") }) + + t.Run("options", func(t *testing.T) { + nw := &network.Network{ + Name: nwName, + State: "present", + Options: map[string]interface{}{"com.docker.network.bridge.enable_icc": "true"}, + Force: true, + } + c := &mockClient{} + nw.SetClient(c) + c.On("FindNetwork", nwName).Return(&dc.Network{ + Name: nwName, + Driver: network.DefaultDriver, + Options: nil, + }, nil) + + status, err := nw.Check(fakerenderer.New()) + require.NoError(t, err) + assert.True(t, status.HasChanges()) + assert.True(t, len(status.Diffs()) > 1) + comparison.AssertDiff(t, status.Diffs(), nwName, "present", "present") + comparison.AssertDiff(t, status.Diffs(), "options", "", "com.docker.network.bridge.enable_icc=true") + }) }) }) diff --git a/samples/dockerNetwork.hcl b/samples/dockerNetwork.hcl index 2ac4cab74..65a1ce36e 100644 --- a/samples/dockerNetwork.hcl +++ b/samples/dockerNetwork.hcl @@ -6,5 +6,9 @@ docker.network "test-network" { environment = "test" } + options { + "com.docker.network.bridge.enable_icc" = "true" + } + force = true } From 04e9ca8150bf041ba8201a20f72d3e6b7747ae7e Mon Sep 17 00:00:00 2001 From: ryane Date: Fri, 4 Nov 2016 08:11:13 -0400 Subject: [PATCH 03/14] docker.network: ipam config support --- resource/docker/network/network.go | 71 +++++++++++++++++++++++-- resource/docker/network/network_test.go | 27 +++++++++- resource/docker/network/preparer.go | 66 +++++++++++++++++++++++ samples/dockerNetwork.hcl | 15 +++++- 4 files changed, 173 insertions(+), 6 deletions(-) diff --git a/resource/docker/network/network.go b/resource/docker/network/network.go index 43e8c7928..3b3066465 100644 --- a/resource/docker/network/network.go +++ b/resource/docker/network/network.go @@ -37,6 +37,9 @@ const ( // DefaultDriver is the default network driver DefaultDriver = "bridge" + + // DefaultIPAMDriver is the default IPAM driver + DefaultIPAMDriver = "default" ) // Network is responsible for managing docker volumes @@ -48,6 +51,7 @@ type Network struct { Driver string Labels map[string]string Options map[string]interface{} + IPAM dc.IPAMOptions State State Force bool } @@ -65,9 +69,7 @@ func (n *Network) Check(resource.Renderer) (resource.TaskStatus, error) { n.AddDifference(n.Name, string(networkState(nw)), string(n.State), "") if n.State == StatePresent && nw != nil && n.Force { - n.AddDifference("labels", mapCompareStr(nw.Labels), mapCompareStr(n.Labels), "") - n.AddDifference("driver", nw.Driver, n.Driver, DefaultDriver) - n.AddDifference("options", mapCompareStr(nw.Options), mapCompareStr(toStrMap(n.Options)), "") + n.diffNetwork(nw) } if resource.AnyChanges(n.Differences) { @@ -111,7 +113,9 @@ func (n *Network) Apply() (resource.TaskStatus, error) { Driver: n.Driver, Labels: n.Labels, Options: n.Options, + IPAM: n.IPAM, } + nw, err = n.client.CreateNetwork(opts) if err != nil { n.RaiseLevel(resource.StatusFatal) @@ -140,6 +144,24 @@ func (n *Network) SetClient(client docker.NetworkClient) { n.client = client } +func (n *Network) diffNetwork(nw *dc.Network) { + n.AddDifference("labels", mapCompareStr(nw.Labels), mapCompareStr(n.Labels), "") + n.AddDifference("driver", nw.Driver, n.Driver, DefaultDriver) + n.AddDifference("options", mapCompareStr(nw.Options), mapCompareStr(toStrMap(n.Options)), "") + n.AddDifference("ipam_driver", nw.IPAM.Driver, n.IPAM.Driver, DefaultIPAMDriver) + + // we cannot reliably detect a diff of the ipam config if the desired ipam + // config is the default ([]) but the actual ipam config has a single + // customized entry + if len(n.IPAM.Config) > 0 || len(nw.IPAM.Config) > 1 { + actualIPAMConfigs := IPAMConfigs(nw.IPAM.Config) + sort.Sort(actualIPAMConfigs) + expectedIPAMConfigs := IPAMConfigs(n.IPAM.Config) + sort.Sort(expectedIPAMConfigs) + n.AddDifference("ipam_config", actualIPAMConfigs.String(), expectedIPAMConfigs.String(), "") + } +} + func networkState(nw *dc.Network) State { if nw != nil { return StatePresent @@ -165,3 +187,46 @@ func mapCompareStr(m map[string]string) string { sort.Strings(pairs) return strings.Join(pairs, ", ") } + +// IPAMConfigs is a slice of dc.IPAMConfig +type IPAMConfigs []dc.IPAMConfig + +// Len implements the sort interface for IPAMConfigs +func (ic IPAMConfigs) Len() int { return len(ic) } + +// Swap implements the sort interface for IPAMConfigs +func (ic IPAMConfigs) Swap(i, j int) { ic[i], ic[j] = ic[j], ic[i] } + +// Less implements the sort interface for IPAMConfigs +func (ic IPAMConfigs) Less(i, j int) bool { return ic[i].Subnet < ic[j].Subnet } + +// IPAMConfigString returns a string representation of the IPAMConfigs slice +func (ic IPAMConfigs) String() string { + var configStrs []string + for _, c := range ic { + configStrs = append(configStrs, ipamConfigString(c)) + } + return strings.Join(configStrs, "\n") +} + +func ipamConfigString(c dc.IPAMConfig) string { + var parts []string + + if c.Subnet != "" { + parts = append(parts, fmt.Sprintf("subnet: %s", c.Subnet)) + } + + if c.Gateway != "" { + parts = append(parts, fmt.Sprintf("gateway: %s", c.Gateway)) + } + + if c.IPRange != "" { + parts = append(parts, fmt.Sprintf("ip_range: %s", c.IPRange)) + } + + if len(c.AuxAddress) > 0 { + parts = append(parts, fmt.Sprintf("aux_addresses: [%s]", mapCompareStr(c.AuxAddress))) + } + + return strings.Join(parts, ", ") +} diff --git a/resource/docker/network/network_test.go b/resource/docker/network/network_test.go index f75e03ef0..1bbb9a6da 100644 --- a/resource/docker/network/network_test.go +++ b/resource/docker/network/network_test.go @@ -153,7 +153,6 @@ func TestNetworkCheck(t *testing.T) { nw.SetClient(c) c.On("FindNetwork", nwName).Return(&dc.Network{ Name: nwName, - Driver: network.DefaultDriver, Options: nil, }, nil) @@ -164,6 +163,32 @@ func TestNetworkCheck(t *testing.T) { comparison.AssertDiff(t, status.Diffs(), nwName, "present", "present") comparison.AssertDiff(t, status.Diffs(), "options", "", "com.docker.network.bridge.enable_icc=true") }) + + t.Run("ipam options", func(t *testing.T) { + nw := &network.Network{ + Name: nwName, + State: "present", + IPAM: dc.IPAMOptions{ + Driver: network.DefaultIPAMDriver, + Config: []dc.IPAMConfig{ + dc.IPAMConfig{Subnet: "192.168.129.0/24"}, + }, + }, + Force: true, + } + c := &mockClient{} + nw.SetClient(c) + c.On("FindNetwork", nwName).Return(&dc.Network{ + Name: nwName, + }, nil) + + status, err := nw.Check(fakerenderer.New()) + require.NoError(t, err) + assert.True(t, status.HasChanges()) + assert.True(t, len(status.Diffs()) > 1) + comparison.AssertDiff(t, status.Diffs(), nwName, "present", "present") + comparison.AssertDiff(t, status.Diffs(), "ipam_config", "", "subnet: 192.168.129.0/24") + }) }) }) diff --git a/resource/docker/network/preparer.go b/resource/docker/network/preparer.go index 81c91f198..dda192c7a 100644 --- a/resource/docker/network/preparer.go +++ b/resource/docker/network/preparer.go @@ -18,6 +18,7 @@ import ( "github.com/asteris-llc/converge/load/registry" "github.com/asteris-llc/converge/resource" "github.com/asteris-llc/converge/resource/docker" + dc "github.com/fsouza/go-dockerclient" ) // Preparer for docker networks @@ -37,6 +38,20 @@ type Preparer struct { // driver specific options Options map[string]interface{} `hcl:"options"` + // ip address management driver + IPAMDriver string `hcl:"ipam_driver"` + + // custom IPAM configuration. multiple IPAM configurations are permitted. Each + // IPAM configuration block should contain one or more of the following items: + // + // subnet: subnet in CIDR format + // gateway: ipv4 or ipv6 gateway for the corresponding subnet + // ip_range: container ips are allocated from this sub-ranges (CIDR format) + // aux_address: auxiliary ipv4 or ipv6 addresses used by the network driver. + // Aux addresses are specified as a map with a name key and an ip + // address value + IPAMConfig []ipamConfigMap `hcl:"ipam_config"` + // indicates whether the volume should exist. State State `hcl:"state" valid_values:"present,absent"` @@ -66,6 +81,7 @@ func (p *Preparer) Prepare(render resource.Renderer) (resource.Task, error) { Driver: p.Driver, Labels: p.Labels, Options: p.Options, + IPAM: p.buildIPAMOptions(), State: p.State, Force: p.Force, } @@ -73,6 +89,56 @@ func (p *Preparer) Prepare(render resource.Renderer) (resource.Task, error) { return nw, nil } +func (p *Preparer) buildIPAMOptions() dc.IPAMOptions { + ipamOptions := dc.IPAMOptions{ + Driver: p.IPAMDriver, + } + + for _, ipamConfigMap := range p.IPAMConfig { + ipamConfig := ipamConfigMap.IPAMConfig() + if ipamConfig.Subnet != "" || len(ipamConfig.AuxAddress) > 0 { + ipamOptions.Config = append(ipamOptions.Config, ipamConfig) + } + } + + return ipamOptions +} + +type ipamConfigMap map[string]interface{} + +func (i ipamConfigMap) IPAMConfig() dc.IPAMConfig { + config := dc.IPAMConfig{} + subnet := i.value("subnet") + if subnet != "" { + config.Subnet = subnet + config.Gateway = i.value("gateway") + config.IPRange = i.value("ip_range") + } + + if val, ok := i["aux_addresses"]; ok { + if auxMap, ok := val.(map[string]interface{}); ok { + auxAddrs := make(map[string]string) + for name, ipval := range auxMap { + if ip, ok := ipval.(string); ok { + auxAddrs[name] = ip + } + } + config.AuxAddress = auxAddrs + } + } + + return config +} + +func (i ipamConfigMap) value(key string) string { + if val, ok := i[key]; ok { + if strval, ok := val.(string); ok { + return strval + } + } + return "" +} + func init() { registry.Register("docker.network", (*Preparer)(nil), (*Network)(nil)) } diff --git a/samples/dockerNetwork.hcl b/samples/dockerNetwork.hcl index 65a1ce36e..cc526bb3e 100644 --- a/samples/dockerNetwork.hcl +++ b/samples/dockerNetwork.hcl @@ -1,14 +1,25 @@ docker.network "test-network" { name = "test-network" state = "present" + force = true labels { environment = "test" } options { - "com.docker.network.bridge.enable_icc" = "true" + "com.docker.network.bridge.enable_icc" = "true" } - force = true + ipam_driver = "default" + + ipam_config { + subnet = "192.168.129.0/24" + gateway = "192.168.129.1" + + aux_addresses { + router = "192.168.129.40" + printer = "192.168.129.41" + } + } } From f7d77a6448aa86d33aacf7e79fead0a9d9f2a58f Mon Sep 17 00:00:00 2001 From: ryane Date: Fri, 4 Nov 2016 08:28:42 -0400 Subject: [PATCH 04/14] docker.network: internal / ipv6 support --- resource/docker/network/network.go | 31 ++++++++++------- resource/docker/network/network_test.go | 44 +++++++++++++++++++++++++ resource/docker/network/preparer.go | 22 +++++++++---- samples/dockerNetwork.hcl | 4 ++- 4 files changed, 81 insertions(+), 20 deletions(-) diff --git a/resource/docker/network/network.go b/resource/docker/network/network.go index 3b3066465..90001f3c1 100644 --- a/resource/docker/network/network.go +++ b/resource/docker/network/network.go @@ -17,6 +17,7 @@ package network import ( "fmt" "sort" + "strconv" "strings" "github.com/asteris-llc/converge/helpers/transform" @@ -47,13 +48,15 @@ type Network struct { *resource.Status client docker.NetworkClient - Name string - Driver string - Labels map[string]string - Options map[string]interface{} - IPAM dc.IPAMOptions - State State - Force bool + Name string + Driver string + Labels map[string]string + Options map[string]interface{} + IPAM dc.IPAMOptions + Internal bool + IPv6 bool + State State + Force bool } // Check system for docker network @@ -109,11 +112,13 @@ func (n *Network) Apply() (resource.TaskStatus, error) { } opts := dc.CreateNetworkOptions{ - Name: n.Name, - Driver: n.Driver, - Labels: n.Labels, - Options: n.Options, - IPAM: n.IPAM, + Name: n.Name, + Driver: n.Driver, + Labels: n.Labels, + Options: n.Options, + IPAM: n.IPAM, + Internal: n.Internal, + EnableIPv6: n.IPv6, } nw, err = n.client.CreateNetwork(opts) @@ -148,6 +153,8 @@ func (n *Network) diffNetwork(nw *dc.Network) { n.AddDifference("labels", mapCompareStr(nw.Labels), mapCompareStr(n.Labels), "") n.AddDifference("driver", nw.Driver, n.Driver, DefaultDriver) n.AddDifference("options", mapCompareStr(nw.Options), mapCompareStr(toStrMap(n.Options)), "") + n.AddDifference("internal", strconv.FormatBool(nw.Internal), strconv.FormatBool(n.Internal), "false") + n.AddDifference("ipv6", strconv.FormatBool(nw.EnableIPv6), strconv.FormatBool(n.IPv6), "false") n.AddDifference("ipam_driver", nw.IPAM.Driver, n.IPAM.Driver, DefaultIPAMDriver) // we cannot reliably detect a diff of the ipam config if the desired ipam diff --git a/resource/docker/network/network_test.go b/resource/docker/network/network_test.go index 1bbb9a6da..a494a5bc6 100644 --- a/resource/docker/network/network_test.go +++ b/resource/docker/network/network_test.go @@ -189,6 +189,50 @@ func TestNetworkCheck(t *testing.T) { comparison.AssertDiff(t, status.Diffs(), nwName, "present", "present") comparison.AssertDiff(t, status.Diffs(), "ipam_config", "", "subnet: 192.168.129.0/24") }) + + t.Run("internal", func(t *testing.T) { + nw := &network.Network{ + Name: nwName, + State: "present", + Internal: true, + Force: true, + } + c := &mockClient{} + nw.SetClient(c) + c.On("FindNetwork", nwName).Return(&dc.Network{ + Name: nwName, + Internal: false, + }, nil) + + status, err := nw.Check(fakerenderer.New()) + require.NoError(t, err) + assert.True(t, status.HasChanges()) + assert.True(t, len(status.Diffs()) > 1) + comparison.AssertDiff(t, status.Diffs(), nwName, "present", "present") + comparison.AssertDiff(t, status.Diffs(), "internal", "false", "true") + }) + + t.Run("ipv6", func(t *testing.T) { + nw := &network.Network{ + Name: nwName, + State: "present", + IPv6: true, + Force: true, + } + c := &mockClient{} + nw.SetClient(c) + c.On("FindNetwork", nwName).Return(&dc.Network{ + Name: nwName, + EnableIPv6: false, + }, nil) + + status, err := nw.Check(fakerenderer.New()) + require.NoError(t, err) + assert.True(t, status.HasChanges()) + assert.True(t, len(status.Diffs()) > 1) + comparison.AssertDiff(t, status.Diffs(), nwName, "present", "present") + comparison.AssertDiff(t, status.Diffs(), "ipv6", "false", "true") + }) }) }) diff --git a/resource/docker/network/preparer.go b/resource/docker/network/preparer.go index dda192c7a..f7874854a 100644 --- a/resource/docker/network/preparer.go +++ b/resource/docker/network/preparer.go @@ -52,6 +52,12 @@ type Preparer struct { // address value IPAMConfig []ipamConfigMap `hcl:"ipam_config"` + // restricts external access to the network + Internal bool `hcl:"internal"` + + // enable ipv6 networking + IPv6 bool `hcl:"ipv6"` + // indicates whether the volume should exist. State State `hcl:"state" valid_values:"present,absent"` @@ -77,13 +83,15 @@ func (p *Preparer) Prepare(render resource.Renderer) (resource.Task, error) { } nw := &Network{ - Name: p.Name, - Driver: p.Driver, - Labels: p.Labels, - Options: p.Options, - IPAM: p.buildIPAMOptions(), - State: p.State, - Force: p.Force, + Name: p.Name, + Driver: p.Driver, + Labels: p.Labels, + Options: p.Options, + IPAM: p.buildIPAMOptions(), + Internal: p.Internal, + IPv6: p.IPv6, + State: p.State, + Force: p.Force, } nw.SetClient(dockerClient) return nw, nil diff --git a/samples/dockerNetwork.hcl b/samples/dockerNetwork.hcl index cc526bb3e..d7ff75bc5 100644 --- a/samples/dockerNetwork.hcl +++ b/samples/dockerNetwork.hcl @@ -8,9 +8,11 @@ docker.network "test-network" { } options { - "com.docker.network.bridge.enable_icc" = "true" + "com.docker.network.bridge.enable_icc" = "true" } + internal = false + ipv6 = false ipam_driver = "default" ipam_config { From fcbcda2927cc2512fdc8542e505ae1968523d116 Mon Sep 17 00:00:00 2001 From: ryane Date: Fri, 4 Nov 2016 08:46:48 -0400 Subject: [PATCH 05/14] docker.network: validate name --- resource/docker/network/preparer.go | 12 +++++++++--- resource/docker/network/preparer_test.go | 8 ++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/resource/docker/network/preparer.go b/resource/docker/network/preparer.go index f7874854a..86c5c4e9c 100644 --- a/resource/docker/network/preparer.go +++ b/resource/docker/network/preparer.go @@ -15,6 +15,8 @@ package network import ( + "errors" + "github.com/asteris-llc/converge/load/registry" "github.com/asteris-llc/converge/resource" "github.com/asteris-llc/converge/resource/docker" @@ -69,9 +71,8 @@ type Preparer struct { // Prepare a docker network func (p *Preparer) Prepare(render resource.Renderer) (resource.Task, error) { - dockerClient, err := docker.NewDockerClient() - if err != nil { - return nil, err + if p.Name == "" { + return nil, errors.New("name must be provided") } if p.Driver == "" { @@ -82,6 +83,11 @@ func (p *Preparer) Prepare(render resource.Renderer) (resource.Task, error) { p.State = "present" } + dockerClient, err := docker.NewDockerClient() + if err != nil { + return nil, err + } + nw := &Network{ Name: p.Name, Driver: p.Driver, diff --git a/resource/docker/network/preparer_test.go b/resource/docker/network/preparer_test.go index dd664dea7..7efdecc96 100644 --- a/resource/docker/network/preparer_test.go +++ b/resource/docker/network/preparer_test.go @@ -33,6 +33,14 @@ func TestPreparerInterface(t *testing.T) { // TestPreparerPrepare tests the Prepare function func TestPreparerPrepare(t *testing.T) { + t.Run("name is required", func(t *testing.T) { + p := &network.Preparer{Name: ""} + _, err := p.Prepare(fakerenderer.New()) + if assert.Error(t, err) { + assert.EqualError(t, err, "name must be provided") + } + }) + t.Run("state defaults to present", func(t *testing.T) { p := &network.Preparer{Name: "test-network"} task, err := p.Prepare(fakerenderer.New()) From b9689e2361f1677f4ae4efcd1f4d36486641df2c Mon Sep 17 00:00:00 2001 From: ryane Date: Fri, 4 Nov 2016 08:47:19 -0400 Subject: [PATCH 06/14] docker.network: docs --- docs/content/resources/docker.network.md | 104 +++++++++++++++++++++++ docs/sources.csv | 1 + resource/docker/network/preparer.go | 12 +-- 3 files changed, 111 insertions(+), 6 deletions(-) create mode 100644 docs/content/resources/docker.network.md diff --git a/docs/content/resources/docker.network.md b/docs/content/resources/docker.network.md new file mode 100644 index 000000000..6c20f626f --- /dev/null +++ b/docs/content/resources/docker.network.md @@ -0,0 +1,104 @@ +--- +title: "docker.network" +slug: "docker-network" +date: "2016-11-04T08:35:32-04:00" +menu: + main: + parent: resources +--- + + +Network is responsible for managing Docker networks. It assumes that there is +already a Docker daemon running on the system. + + +## Example + +```hcl +docker.network "test-network" { + name = "test-network" + state = "present" + force = true + + labels { + environment = "test" + } + + options { + "com.docker.network.bridge.enable_icc" = "true" + } + + internal = false + ipv6 = false + ipam_driver = "default" + + ipam_config { + subnet = "192.168.129.0/24" + gateway = "192.168.129.1" + + aux_addresses { + router = "192.168.129.40" + printer = "192.168.129.41" + } + } +} + +``` + + +## Parameters + +- `name` (required string) + + name of the network + +- `driver` (string) + + network driver. default: bridge + +- `labels` (map of string to string) + + labels to set on the network + +- `options` (map of string to anything) + + driver specific options + +- `ipam_driver` (string) + + ip address management driver + +- `ipam_config` (list of ipamConfigMaps) + + custom IPAM configuration. multiple IPAM configurations are permitted. Each +IPAM configuration block should contain one or more of the following items: + + * subnet: subnet in CIDR format + * gateway: ipv4 or ipv6 gateway for the corresponding subnet + * ip_range: container ips are allocated from this sub-ranges (CIDR format) + * aux_address: auxiliary ipv4 or ipv6 addresses used by the network driver. + Aux addresses are specified as a map with a name key and an IP + address value + +- `internal` (bool) + + restricts external access to the network + +- `ipv6` (bool) + + enable ipv6 networking + +- `state` (State) + + + Valid values: `present` and `absent` + + indicates whether the volume should exist. + +- `force` (bool) + + indicates whether or not the volume will be recreated if the state is not +what is expected. By default, the module will only check to see if the +volume exists. Specified as a boolean value + + diff --git a/docs/sources.csv b/docs/sources.csv index 880a2576f..402dccdd8 100644 --- a/docs/sources.csv +++ b/docs/sources.csv @@ -1,6 +1,7 @@ docker.container,../resource/docker/container/preparer.go,../samples/dockerContainer.hcl,Preparer docker.image,../resource/docker/image/preparer.go,../samples/dockerImage.hcl,Preparer docker.volume,../resource/docker/volume/preparer.go,../samples/dockerVolume.hcl,Preparer +docker.network,../resource/docker/network/preparer.go,../samples/dockerNetwork.hcl,Preparer file.content,../resource/file/content/preparer.go,../samples/fileContent.hcl,Preparer file.directory,../resource/file/directory/preparer.go,../samples/fileDirectory.hcl,Preparer file.mode,../resource/file/mode/preparer.go,../samples/fileMode.hcl,Preparer diff --git a/resource/docker/network/preparer.go b/resource/docker/network/preparer.go index 86c5c4e9c..e34ce0e93 100644 --- a/resource/docker/network/preparer.go +++ b/resource/docker/network/preparer.go @@ -46,12 +46,12 @@ type Preparer struct { // custom IPAM configuration. multiple IPAM configurations are permitted. Each // IPAM configuration block should contain one or more of the following items: // - // subnet: subnet in CIDR format - // gateway: ipv4 or ipv6 gateway for the corresponding subnet - // ip_range: container ips are allocated from this sub-ranges (CIDR format) - // aux_address: auxiliary ipv4 or ipv6 addresses used by the network driver. - // Aux addresses are specified as a map with a name key and an ip - // address value + // * subnet: subnet in CIDR format + // * gateway: ipv4 or ipv6 gateway for the corresponding subnet + // * ip_range: container ips are allocated from this sub-ranges (CIDR format) + // * aux_address: auxiliary ipv4 or ipv6 addresses used by the network driver. + // Aux addresses are specified as a map with a name key and an IP + // address value IPAMConfig []ipamConfigMap `hcl:"ipam_config"` // restricts external access to the network From 242a0f92b13dd1912ca32f10187dc617a5c01209 Mon Sep 17 00:00:00 2001 From: ryane Date: Fri, 4 Nov 2016 10:16:46 -0400 Subject: [PATCH 07/14] docker.container: refactor container tests --- resource/docker/container/container_test.go | 1195 ++++++++++--------- 1 file changed, 617 insertions(+), 578 deletions(-) diff --git a/resource/docker/container/container_test.go b/resource/docker/container/container_test.go index afebe47f1..28c677bf3 100644 --- a/resource/docker/container/container_test.go +++ b/resource/docker/container/container_test.go @@ -21,6 +21,7 @@ import ( "github.com/asteris-llc/converge/helpers/comparison" "github.com/asteris-llc/converge/helpers/fakerenderer" + "github.com/asteris-llc/converge/helpers/logging" "github.com/asteris-llc/converge/resource" "github.com/asteris-llc/converge/resource/docker/container" dc "github.com/fsouza/go-dockerclient" @@ -33,602 +34,635 @@ func TestContainerInterface(t *testing.T) { assert.Implements(t, (*resource.Task)(nil), new(container.Container)) } -func TestContainerCheckContainerNotFound(t *testing.T) { +// TestContainerCheck tests the Container.Check function +func TestContainerCheck(t *testing.T) { t.Parallel() - - c := &fakeAPIClient{ - FindContainerFunc: func(string) (*dc.Container, error) { - return nil, nil - }, - } - - name := "nginx" - container := &container.Container{Force: true, Name: name} - container.SetClient(c) - - status, err := container.Check(context.Background(), fakerenderer.New()) - assert.NoError(t, err) - assert.True(t, status.HasChanges()) - comparison.AssertDiff(t, status.Diffs(), "name", "", name) -} - -func TestContainerCheckContainerFindContainerError(t *testing.T) { - t.Parallel() - - c := &fakeAPIClient{ - FindContainerFunc: func(string) (*dc.Container, error) { - return nil, errors.New("find container failed") - }, - } - - container := &container.Container{Force: true, Name: "nginx"} - container.SetClient(c) - - status, err := container.Check(context.Background(), fakerenderer.New()) - if assert.Error(t, err) { - assert.EqualError(t, err, "find container failed") - } - assert.Equal(t, resource.StatusFatal, status.StatusCode()) - assert.False(t, status.HasChanges()) -} - -func TestContainerCheckContainerNoChange(t *testing.T) { - t.Parallel() - - c := &fakeAPIClient{ - FindContainerFunc: func(string) (*dc.Container, error) { - return &dc.Container{ - Name: "nginx", - State: dc.State{Status: "running"}, - Config: &dc.Config{}}, nil - }, - FindImageFunc: func(string) (*dc.Image, error) { - return &dc.Image{Config: &dc.Config{}}, nil - }, - } - - container := &container.Container{Force: true, Name: "nginx"} - container.SetClient(c) - - status, err := container.Check(context.Background(), fakerenderer.New()) - - assert.Nil(t, err) - assert.False(t, status.HasChanges()) -} - -func TestContainerCheckStatusNeedsChange(t *testing.T) { - t.Parallel() - - c := &fakeAPIClient{ - // the existing container is running the "nginx" command - FindContainerFunc: func(name string) (*dc.Container, error) { - return &dc.Container{ - Name: name, - Config: &dc.Config{ - Cmd: []string{"nginx"}, - }, - State: dc.State{Status: "exited"}, - }, nil - }, - FindImageFunc: func(repoTag string) (*dc.Image, error) { - return &dc.Image{Config: &dc.Config{}}, nil - }, - } - - container := &container.Container{Force: true, Name: "nginx"} - container.SetClient(c) - - status, err := container.Check(context.Background(), fakerenderer.New()) - assert.NoError(t, err) - assert.True(t, status.HasChanges()) - comparison.AssertDiff(t, status.Diffs(), "status", "exited", "running") -} - -func TestContainerCheckStatusNoChange(t *testing.T) { - t.Parallel() - - c := &fakeAPIClient{ - FindContainerFunc: func(name string) (*dc.Container, error) { - return &dc.Container{ - Name: name, - Config: &dc.Config{}, - State: dc.State{Status: "created"}, - }, nil - }, - FindImageFunc: func(repoTag string) (*dc.Image, error) { - return &dc.Image{Config: &dc.Config{}}, nil - }, - } - - container := &container.Container{Force: true, Name: "nginx", CStatus: "created"} - container.SetClient(c) - - status, err := container.Check(context.Background(), fakerenderer.New()) - assert.NoError(t, err) - assert.False(t, status.HasChanges()) - comparison.AssertDiff(t, status.Diffs(), "status", "created", "created") -} - -func TestContainerCheckCommandNeedsChange(t *testing.T) { - // This test simulates a running container with a command that is different - // than the specified command - t.Parallel() - - c := &fakeAPIClient{ - // the existing container is running the "nginx" command - FindContainerFunc: func(name string) (*dc.Container, error) { - return &dc.Container{ - Name: name, - Config: &dc.Config{ - Cmd: []string{"nginx"}, - }, - }, nil - }, - FindImageFunc: func(repoTag string) (*dc.Image, error) { - return &dc.Image{Config: &dc.Config{}}, nil - }, - } - - container := &container.Container{ - Force: true, - Name: "nginx", - Command: []string{"nginx", "-g", "daemon", "off;"}, - } - container.SetClient(c) - - status, err := container.Check(context.Background(), fakerenderer.New()) - assert.NoError(t, err) - assert.True(t, status.HasChanges()) - comparison.AssertDiff(t, status.Diffs(), "command", "nginx", "nginx -g daemon off;") -} - -func TestContainerCheckEmptyCommandNeedsChange(t *testing.T) { - // Specifying an empty Command means we want to use the default image command. - // This test simulates a running container with a command that is different - // than the image default. - t.Parallel() - - c := &fakeAPIClient{ - // the existing container is running the "nginx" command - FindContainerFunc: func(name string) (*dc.Container, error) { - return &dc.Container{ - Name: name, - Config: &dc.Config{ - Cmd: []string{"nginx"}, - }, - }, nil - }, - // the image has a default command of "nginx -g daemon off;" - FindImageFunc: func(repoTag string) (*dc.Image, error) { - return &dc.Image{ - Config: &dc.Config{ - Cmd: []string{"nginx", "-g", "daemon off;"}, - }, - }, nil - }, - } - - // the resource uses an empty command implying that the default should be - // running - container := &container.Container{ - Force: true, - Name: "nginx", - Command: []string{}, - } - container.SetClient(c) - - status, err := container.Check(context.Background(), fakerenderer.New()) - assert.NoError(t, err) - assert.True(t, status.HasChanges()) - comparison.AssertDiff(t, status.Diffs(), "command", "nginx", "nginx -g daemon off;") -} - -func TestContainerCheckImageNeedsChange(t *testing.T) { - // This test simulates a running container with an image that is different - // than the specified image. - t.Parallel() - - c := &fakeAPIClient{ - // the existing container is running the "nginx" image - FindContainerFunc: func(name string) (*dc.Container, error) { - return &dc.Container{ - Name: name, - Image: "nginx", - Config: &dc.Config{}, - }, nil - }, - FindImageFunc: func(repoTag string) (*dc.Image, error) { - return &dc.Image{ - RepoTags: []string{"nginx"}, - Config: &dc.Config{ - Image: "nginx", - }, - }, nil - }, - } - - // the resource uses a different image - container := &container.Container{Force: true, Name: "nginx", Image: "busybox"} - container.SetClient(c) - - status, err := container.Check(context.Background(), fakerenderer.New()) - assert.NoError(t, err) - assert.True(t, status.HasChanges()) - comparison.AssertDiff(t, status.Diffs(), "image", "nginx", "busybox") -} - -func TestContainerCheckEntrypointNeedsChange(t *testing.T) { - t.Parallel() - - c := &fakeAPIClient{ - // the existing container defaults to the "start" entrypoint - FindContainerFunc: func(name string) (*dc.Container, error) { - return &dc.Container{ - Name: name, - Config: &dc.Config{ + defer logging.HideLogs(t)() + + t.Run("container not found", func(t *testing.T) { + c := &fakeAPIClient{ + FindContainerFunc: func(string) (*dc.Container, error) { + return nil, nil + }, + } + + name := "nginx" + container := &container.Container{Force: true, Name: name} + container.SetClient(c) + + status, err := container.Check(context.Background(), fakerenderer.New()) + assert.NoError(t, err) + assert.True(t, status.HasChanges()) + comparison.AssertDiff(t, status.Diffs(), "name", "", name) + }) + + t.Run("find container error", func(t *testing.T) { + c := &fakeAPIClient{ + FindContainerFunc: func(string) (*dc.Container, error) { + return nil, errors.New("find container failed") + }, + } + + container := &container.Container{Force: true, Name: "nginx"} + container.SetClient(c) + + status, err := container.Check(context.Background(), fakerenderer.New()) + if assert.Error(t, err) { + assert.EqualError(t, err, "find container failed") + } + assert.Equal(t, resource.StatusFatal, status.StatusCode()) + assert.False(t, status.HasChanges()) + }) + + t.Run("no change", func(t *testing.T) { + c := &fakeAPIClient{ + FindContainerFunc: func(string) (*dc.Container, error) { + return &dc.Container{ + Name: "nginx", + State: dc.State{Status: "running"}, + Config: &dc.Config{}}, nil + }, + FindImageFunc: func(string) (*dc.Image, error) { + return &dc.Image{Config: &dc.Config{}}, nil + }, + } + + container := &container.Container{Force: true, Name: "nginx"} + container.SetClient(c) + + status, err := container.Check(context.Background(), fakerenderer.New()) + + assert.Nil(t, err) + assert.False(t, status.HasChanges()) + }) + + t.Run("status change", func(t *testing.T) { + c := &fakeAPIClient{ + // the existing container is running the "nginx" command + FindContainerFunc: func(name string) (*dc.Container, error) { + return &dc.Container{ + Name: name, + Config: &dc.Config{ + Cmd: []string{"nginx"}, + }, + State: dc.State{Status: "exited"}, + }, nil + }, + FindImageFunc: func(repoTag string) (*dc.Image, error) { + return &dc.Image{Config: &dc.Config{}}, nil + }, + } + + container := &container.Container{Force: true, Name: "nginx"} + container.SetClient(c) + + status, err := container.Check(context.Background(), fakerenderer.New()) + assert.NoError(t, err) + assert.True(t, status.HasChanges()) + comparison.AssertDiff(t, status.Diffs(), "status", "exited", "running") + }) + + t.Run("status no change", func(t *testing.T) { + c := &fakeAPIClient{ + FindContainerFunc: func(name string) (*dc.Container, error) { + return &dc.Container{ + Name: name, + Config: &dc.Config{}, + State: dc.State{Status: "created"}, + }, nil + }, + FindImageFunc: func(repoTag string) (*dc.Image, error) { + return &dc.Image{Config: &dc.Config{}}, nil + }, + } + + container := &container.Container{Force: true, Name: "nginx", CStatus: "created"} + container.SetClient(c) + + status, err := container.Check(context.Background(), fakerenderer.New()) + assert.NoError(t, err) + assert.False(t, status.HasChanges()) + comparison.AssertDiff(t, status.Diffs(), "status", "created", "created") + }) + + t.Run("command change", func(t *testing.T) { + // This test simulates a running container with a command that is different + // than the specified command + c := &fakeAPIClient{ + // the existing container is running the "nginx" command + FindContainerFunc: func(name string) (*dc.Container, error) { + return &dc.Container{ + Name: name, + Config: &dc.Config{ + Cmd: []string{"nginx"}, + }, + }, nil + }, + FindImageFunc: func(repoTag string) (*dc.Image, error) { + return &dc.Image{Config: &dc.Config{}}, nil + }, + } + + container := &container.Container{ + Force: true, + Name: "nginx", + Command: []string{"nginx", "-g", "daemon", "off;"}, + } + container.SetClient(c) + + status, err := container.Check(context.Background(), fakerenderer.New()) + assert.NoError(t, err) + assert.True(t, status.HasChanges()) + comparison.AssertDiff(t, status.Diffs(), "command", "nginx", "nginx -g daemon off;") + }) + + t.Run("empty command needs change", func(t *testing.T) { + // Specifying an empty Command means we want to use the default image command. + // This test simulates a running container with a command that is different + // than the image default. + c := &fakeAPIClient{ + // the existing container is running the "nginx" command + FindContainerFunc: func(name string) (*dc.Container, error) { + return &dc.Container{ + Name: name, + Config: &dc.Config{ + Cmd: []string{"nginx"}, + }, + }, nil + }, + // the image has a default command of "nginx -g daemon off;" + FindImageFunc: func(repoTag string) (*dc.Image, error) { + return &dc.Image{ + Config: &dc.Config{ + Cmd: []string{"nginx", "-g", "daemon off;"}, + }, + }, nil + }, + } + + // the resource uses an empty command implying that the default should be + // running + container := &container.Container{ + Force: true, + Name: "nginx", + Command: []string{}, + } + container.SetClient(c) + + status, err := container.Check(context.Background(), fakerenderer.New()) + assert.NoError(t, err) + assert.True(t, status.HasChanges()) + comparison.AssertDiff(t, status.Diffs(), "command", "nginx", "nginx -g daemon off;") + }) + + t.Run("image change", func(t *testing.T) { + // This test simulates a running container with an image that is different + // than the specified image. + c := &fakeAPIClient{ + // the existing container is running the "nginx" image + FindContainerFunc: func(name string) (*dc.Container, error) { + return &dc.Container{ + Name: name, + Image: "nginx", + Config: &dc.Config{}, + }, nil + }, + FindImageFunc: func(repoTag string) (*dc.Image, error) { + return &dc.Image{ + RepoTags: []string{"nginx"}, + Config: &dc.Config{ + Image: "nginx", + }, + }, nil + }, + } + + // the resource uses a different image + container := &container.Container{Force: true, Name: "nginx", Image: "busybox"} + container.SetClient(c) + + status, err := container.Check(context.Background(), fakerenderer.New()) + assert.NoError(t, err) + assert.True(t, status.HasChanges()) + comparison.AssertDiff(t, status.Diffs(), "image", "nginx", "busybox") + }) + + t.Run("entrypoint change", func(t *testing.T) { + c := &fakeAPIClient{ + // the existing container defaults to the "start" entrypoint + FindContainerFunc: func(name string) (*dc.Container, error) { + return &dc.Container{ + Name: name, + Config: &dc.Config{ + Entrypoint: []string{"start"}, + }, + }, nil + }, + FindImageFunc: func(repoTag string) (*dc.Image, error) { + return &dc.Image{Config: &dc.Config{ Entrypoint: []string{"start"}, - }, - }, nil - }, - FindImageFunc: func(repoTag string) (*dc.Image, error) { - return &dc.Image{Config: &dc.Config{ - Entrypoint: []string{"start"}, - }}, nil - }, - } - - container := &container.Container{ - Force: true, - Name: "nginx", - Entrypoint: []string{"/bin/bash", "start"}, - } - container.SetClient(c) - - status, err := container.Check(context.Background(), fakerenderer.New()) - assert.NoError(t, err) - assert.True(t, status.HasChanges()) - comparison.AssertDiff(t, status.Diffs(), "entrypoint", "start", "/bin/bash start") -} - -func TestContainerCheckWorkingDirNeedsChange(t *testing.T) { - t.Parallel() - - c := &fakeAPIClient{ - FindContainerFunc: func(name string) (*dc.Container, error) { - return &dc.Container{ - Name: name, - Config: &dc.Config{ - WorkingDir: "/tmp", - }, - }, nil - }, - FindImageFunc: func(repoTag string) (*dc.Image, error) { - return &dc.Image{Config: &dc.Config{}}, nil - }, - } - - container := &container.Container{Force: true, Name: "nginx", WorkingDir: "/tmp/working"} - container.SetClient(c) - - status, err := container.Check(context.Background(), fakerenderer.New()) - assert.NoError(t, err) - assert.True(t, status.HasChanges()) - comparison.AssertDiff(t, status.Diffs(), "working_dir", "/tmp", "/tmp/working") -} - -func TestContainerCheckEnvNeedsChange(t *testing.T) { - t.Parallel() - - c := &fakeAPIClient{ - FindContainerFunc: func(name string) (*dc.Container, error) { - return &dc.Container{ - Name: name, - Config: &dc.Config{ - Env: []string{ - "PATH=/usr/bin", // set from image - "HTTP_PROXY=http://localhost:8080", // set by engine - "no_proxy=*.local", // set by engine - "FROMIMAGE=yes", // set from image - "FOO=BAR", // set in container - "EXTRA=TEST", // set in container + }}, nil + }, + } + + container := &container.Container{ + Force: true, + Name: "nginx", + Entrypoint: []string{"/bin/bash", "start"}, + } + container.SetClient(c) + + status, err := container.Check(context.Background(), fakerenderer.New()) + assert.NoError(t, err) + assert.True(t, status.HasChanges()) + comparison.AssertDiff(t, status.Diffs(), "entrypoint", "start", "/bin/bash start") + }) + + t.Run("working dir change", func(t *testing.T) { + c := &fakeAPIClient{ + FindContainerFunc: func(name string) (*dc.Container, error) { + return &dc.Container{ + Name: name, + Config: &dc.Config{ + WorkingDir: "/tmp", }, - }, - }, nil - }, - FindImageFunc: func(repoTag string) (*dc.Image, error) { - return &dc.Image{ - Config: &dc.Config{ - Env: []string{"PATH=/usr/bin", "FROMIMAGE=yes"}, - }, - }, nil - }, - } - - container := &container.Container{ - Force: true, - Name: "nginx", - Env: []string{ - "BAR=BAZ", // new container var - "FOO=BAR", // existing container var - "PATH=/usr/bin;/usr/sbin", // override image var - "NO_PROXY=*.local, 169.254/16", // override engine var - }, - } - container.SetClient(c) - - status, err := container.Check(context.Background(), fakerenderer.New()) - assert.NoError(t, err) - assert.True(t, status.HasChanges()) - // diff should include the new BAR var and the overridden PATH and NO_PROXY - // vars. The EXTRA var should not be included in the desired state either - comparison.AssertDiff( - t, - status.Diffs(), - "env", - "EXTRA=TEST FOO=BAR PATH=/usr/bin no_proxy=*.local", - "BAR=BAZ FOO=BAR NO_PROXY=*.local, 169.254/16 PATH=/usr/bin;/usr/sbin", - ) -} - -func TestContainerCheckExposeNeedsChange(t *testing.T) { - t.Parallel() - - c := &fakeAPIClient{ - FindContainerFunc: func(name string) (*dc.Container, error) { - return &dc.Container{ - Name: name, - Config: &dc.Config{ - ExposedPorts: map[dc.Port]struct{}{ - "80/tcp": struct{}{}, - "443/tcp": struct{}{}, + }, nil + }, + FindImageFunc: func(repoTag string) (*dc.Image, error) { + return &dc.Image{Config: &dc.Config{}}, nil + }, + } + + container := &container.Container{Force: true, Name: "nginx", WorkingDir: "/tmp/working"} + container.SetClient(c) + + status, err := container.Check(context.Background(), fakerenderer.New()) + assert.NoError(t, err) + assert.True(t, status.HasChanges()) + comparison.AssertDiff(t, status.Diffs(), "working_dir", "/tmp", "/tmp/working") + }) + + t.Run("env change", func(t *testing.T) { + c := &fakeAPIClient{ + FindContainerFunc: func(name string) (*dc.Container, error) { + return &dc.Container{ + Name: name, + Config: &dc.Config{ + Env: []string{ + "PATH=/usr/bin", // set from image + "HTTP_PROXY=http://localhost:8080", // set by engine + "no_proxy=*.local", // set by engine + "FROMIMAGE=yes", // set from image + "FOO=BAR", // set in container + "EXTRA=TEST", // set in container + }, }, - }, - }, nil - }, - FindImageFunc: func(repoTag string) (*dc.Image, error) { - return &dc.Image{ - Config: &dc.Config{ - ExposedPorts: map[dc.Port]struct{}{ - "80/tcp": struct{}{}, - "443/tcp": struct{}{}, + }, nil + }, + FindImageFunc: func(repoTag string) (*dc.Image, error) { + return &dc.Image{ + Config: &dc.Config{ + Env: []string{"PATH=/usr/bin", "FROMIMAGE=yes"}, }, - }, - }, nil - }, - } - - container := &container.Container{Force: true, Name: "nginx", Expose: []string{"8001", "8002/udp"}} - container.SetClient(c) - - status, err := container.Check(context.Background(), fakerenderer.New()) - assert.NoError(t, err) - assert.True(t, status.HasChanges()) - comparison.AssertDiff(t, status.Diffs(), "expose", "443/tcp, 80/tcp", "443/tcp, 80/tcp, 8001/tcp, 8002/udp") -} - -func TestContainerCheckPortsNeedsChange(t *testing.T) { - t.Parallel() - - c := &fakeAPIClient{ - FindContainerFunc: func(name string) (*dc.Container, error) { - return &dc.Container{ - Name: name, - Config: &dc.Config{ - ExposedPorts: map[dc.Port]struct{}{ - "80/tcp": struct{}{}, - "443/tcp": struct{}{}, - "8003/tcp": struct{}{}, + }, nil + }, + } + + container := &container.Container{ + Force: true, + Name: "nginx", + Env: []string{ + "BAR=BAZ", // new container var + "FOO=BAR", // existing container var + "PATH=/usr/bin;/usr/sbin", // override image var + "NO_PROXY=*.local, 169.254/16", // override engine var + }, + } + container.SetClient(c) + + status, err := container.Check(context.Background(), fakerenderer.New()) + assert.NoError(t, err) + assert.True(t, status.HasChanges()) + // diff should include the new BAR var and the overridden PATH and NO_PROXY + // vars. The EXTRA var should not be included in the desired state either + comparison.AssertDiff( + t, + status.Diffs(), + "env", + "EXTRA=TEST FOO=BAR PATH=/usr/bin no_proxy=*.local", + "BAR=BAZ FOO=BAR NO_PROXY=*.local, 169.254/16 PATH=/usr/bin;/usr/sbin", + ) + }) + + t.Run("expose change", func(t *testing.T) { + c := &fakeAPIClient{ + FindContainerFunc: func(name string) (*dc.Container, error) { + return &dc.Container{ + Name: name, + Config: &dc.Config{ + ExposedPorts: map[dc.Port]struct{}{ + "80/tcp": struct{}{}, + "443/tcp": struct{}{}, + }, }, - }, - HostConfig: &dc.HostConfig{ - PortBindings: map[dc.Port][]dc.PortBinding{ - dc.Port("80/tcp"): []dc.PortBinding{dc.PortBinding{HostPort: "8003"}}, + }, nil + }, + FindImageFunc: func(repoTag string) (*dc.Image, error) { + return &dc.Image{ + Config: &dc.Config{ + ExposedPorts: map[dc.Port]struct{}{ + "80/tcp": struct{}{}, + "443/tcp": struct{}{}, + }, }, - }, - }, nil - }, - FindImageFunc: func(repoTag string) (*dc.Image, error) { - return &dc.Image{ - Config: &dc.Config{ - ExposedPorts: map[dc.Port]struct{}{ - "80/tcp": struct{}{}, - "443/tcp": struct{}{}, + }, nil + }, + } + + container := &container.Container{Force: true, Name: "nginx", Expose: []string{"8001", "8002/udp"}} + container.SetClient(c) + + status, err := container.Check(context.Background(), fakerenderer.New()) + assert.NoError(t, err) + assert.True(t, status.HasChanges()) + comparison.AssertDiff(t, status.Diffs(), "expose", "443/tcp, 80/tcp", "443/tcp, 80/tcp, 8001/tcp, 8002/udp") + }) + + t.Run("port change", func(t *testing.T) { + c := &fakeAPIClient{ + FindContainerFunc: func(name string) (*dc.Container, error) { + return &dc.Container{ + Name: name, + Config: &dc.Config{ + ExposedPorts: map[dc.Port]struct{}{ + "80/tcp": struct{}{}, + "443/tcp": struct{}{}, + "8003/tcp": struct{}{}, + }, }, - }, - }, nil - }, - } - - container := &container.Container{ - Force: true, - Name: "nginx", - Expose: []string{"8003", "8005/udp"}, - PortBindings: []string{"127.0.0.1:8000:80", "127.0.0.1::80/tcp", "443:443", "8003:80", "8004:80", "80", "8085/udp"}, - } - container.SetClient(c) - - status, err := container.Check(context.Background(), fakerenderer.New()) - assert.NoError(t, err) - assert.True(t, status.HasChanges()) - comparison.AssertDiff(t, status.Diffs(), "ports", ":8003:80/tcp", "127.0.0.1:8000:80/tcp, 127.0.0.1::80/tcp, :443:443/tcp, :8003:80/tcp, :8004:80/tcp, ::80/tcp, ::8085/udp") -} - -func TestContainerCheckLinksNeedsChange(t *testing.T) { - t.Parallel() - - c := &fakeAPIClient{ - FindContainerFunc: func(name string) (*dc.Container, error) { - return &dc.Container{ - Name: name, - Config: &dc.Config{}, - HostConfig: &dc.HostConfig{ - // no alias - Links: []string{fmt.Sprintf("/redis-server:/%s/redis-server", name)}, - }, - }, nil - }, - FindImageFunc: func(repoTag string) (*dc.Image, error) { - return &dc.Image{ - Config: &dc.Config{}, - }, nil - }, - } - - // include alias for existing link and a acouple of more links - container := &container.Container{ - Force: true, - Name: "nginx", - Links: []string{"redis-server:redis", "memcached", "postgresql:db"}, - } - container.SetClient(c) - - status, err := container.Check(context.Background(), fakerenderer.New()) - assert.NoError(t, err) - assert.True(t, status.HasChanges()) - comparison.AssertDiff(t, status.Diffs(), "links", - "redis-server", - "memcached, postgresql:db, redis-server:redis") -} - -func TestContainerCheckDNSNeedsChange(t *testing.T) { - t.Parallel() - - c := &fakeAPIClient{ - FindContainerFunc: func(name string) (*dc.Container, error) { - return &dc.Container{ - Name: name, - Config: &dc.Config{}, - HostConfig: &dc.HostConfig{ - DNS: []string{}, - }, - }, nil - }, - FindImageFunc: func(repoTag string) (*dc.Image, error) { - return &dc.Image{ - Config: &dc.Config{}, - }, nil - }, - } - - // include alias for existing link and a acouple of more links - container := &container.Container{ - Force: true, - Name: "nginx", - DNS: []string{"8.8.8.8", "8.8.4.4"}, - } - container.SetClient(c) - - status, err := container.Check(context.Background(), fakerenderer.New()) - assert.NoError(t, err) - assert.True(t, status.HasChanges()) - comparison.AssertDiff(t, status.Diffs(), "dns", "", "8.8.8.8, 8.8.4.4") -} - -func TestContainerCheckVolumesNeedsChange(t *testing.T) { - t.Parallel() - - c := &fakeAPIClient{ - FindContainerFunc: func(name string) (*dc.Container, error) { - return &dc.Container{ - Name: name, - Config: &dc.Config{ + HostConfig: &dc.HostConfig{ + PortBindings: map[dc.Port][]dc.PortBinding{ + dc.Port("80/tcp"): []dc.PortBinding{dc.PortBinding{HostPort: "8003"}}, + }, + }, + }, nil + }, + FindImageFunc: func(repoTag string) (*dc.Image, error) { + return &dc.Image{ + Config: &dc.Config{ + ExposedPorts: map[dc.Port]struct{}{ + "80/tcp": struct{}{}, + "443/tcp": struct{}{}, + }, + }, + }, nil + }, + } + + container := &container.Container{ + Force: true, + Name: "nginx", + Expose: []string{"8003", "8005/udp"}, + PortBindings: []string{"127.0.0.1:8000:80", "127.0.0.1::80/tcp", "443:443", "8003:80", "8004:80", "80", "8085/udp"}, + } + container.SetClient(c) + + status, err := container.Check(context.Background(), fakerenderer.New()) + assert.NoError(t, err) + assert.True(t, status.HasChanges()) + comparison.AssertDiff(t, status.Diffs(), "ports", ":8003:80/tcp", "127.0.0.1:8000:80/tcp, 127.0.0.1::80/tcp, :443:443/tcp, :8003:80/tcp, :8004:80/tcp, ::80/tcp, ::8085/udp") + }) + + t.Run("link change", func(t *testing.T) { + c := &fakeAPIClient{ + FindContainerFunc: func(name string) (*dc.Container, error) { + return &dc.Container{ + Name: name, + Config: &dc.Config{}, + HostConfig: &dc.HostConfig{ + // no alias + Links: []string{fmt.Sprintf("/redis-server:/%s/redis-server", name)}, + }, + }, nil + }, + FindImageFunc: func(repoTag string) (*dc.Image, error) { + return &dc.Image{ + Config: &dc.Config{}, + }, nil + }, + } + + // include alias for existing link and a acouple of more links + container := &container.Container{ + Force: true, + Name: "nginx", + Links: []string{"redis-server:redis", "memcached", "postgresql:db"}, + } + container.SetClient(c) + + status, err := container.Check(context.Background(), fakerenderer.New()) + assert.NoError(t, err) + assert.True(t, status.HasChanges()) + comparison.AssertDiff(t, status.Diffs(), "links", + "redis-server", + "memcached, postgresql:db, redis-server:redis") + }) + + t.Run("dns change", func(t *testing.T) { + c := &fakeAPIClient{ + FindContainerFunc: func(name string) (*dc.Container, error) { + return &dc.Container{ + Name: name, + Config: &dc.Config{}, + HostConfig: &dc.HostConfig{ + DNS: []string{}, + }, + }, nil + }, + FindImageFunc: func(repoTag string) (*dc.Image, error) { + return &dc.Image{ + Config: &dc.Config{}, + }, nil + }, + } + + // include alias for existing link and a acouple of more links + container := &container.Container{ + Force: true, + Name: "nginx", + DNS: []string{"8.8.8.8", "8.8.4.4"}, + } + container.SetClient(c) + + status, err := container.Check(context.Background(), fakerenderer.New()) + assert.NoError(t, err) + assert.True(t, status.HasChanges()) + comparison.AssertDiff(t, status.Diffs(), "dns", "", "8.8.8.8, 8.8.4.4") + }) + + t.Run("volume change", func(t *testing.T) { + c := &fakeAPIClient{ + FindContainerFunc: func(name string) (*dc.Container, error) { + return &dc.Container{ + Name: name, + Config: &dc.Config{ + Volumes: map[string]struct{}{ + "/var/log": struct{}{}, + }, + }, + }, nil + }, + FindImageFunc: func(repoTag string) (*dc.Image, error) { + return &dc.Image{Config: &dc.Config{ Volumes: map[string]struct{}{ "/var/log": struct{}{}, }, - }, - }, nil - }, - FindImageFunc: func(repoTag string) (*dc.Image, error) { - return &dc.Image{Config: &dc.Config{ - Volumes: map[string]struct{}{ - "/var/log": struct{}{}, - }, - }}, nil - }, - } - - container := &container.Container{Force: true, Name: "nginx", Volumes: []string{"/var/html"}} - container.SetClient(c) - - status, err := container.Check(context.Background(), fakerenderer.New()) - assert.NoError(t, err) - assert.True(t, status.HasChanges()) - comparison.AssertDiff(t, status.Diffs(), "volumes", "/var/log", "/var/html, /var/log") -} - -func TestContainerCheckBindsNeedsChange(t *testing.T) { - t.Parallel() - - c := &fakeAPIClient{ - FindContainerFunc: func(name string) (*dc.Container, error) { - return &dc.Container{ - Name: name, - Config: &dc.Config{ + }}, nil + }, + } + + container := &container.Container{Force: true, Name: "nginx", Volumes: []string{"/var/html"}} + container.SetClient(c) + + status, err := container.Check(context.Background(), fakerenderer.New()) + assert.NoError(t, err) + assert.True(t, status.HasChanges()) + comparison.AssertDiff(t, status.Diffs(), "volumes", "/var/log", "/var/html, /var/log") + }) + + t.Run("bind change", func(t *testing.T) { + c := &fakeAPIClient{ + FindContainerFunc: func(name string) (*dc.Container, error) { + return &dc.Container{ + Name: name, + Config: &dc.Config{ + Volumes: map[string]struct{}{ + "/var/log": struct{}{}, + }, + }, + HostConfig: &dc.HostConfig{ + Binds: []string{}, + }, + }, nil + }, + FindImageFunc: func(repoTag string) (*dc.Image, error) { + return &dc.Image{Config: &dc.Config{ Volumes: map[string]struct{}{ "/var/log": struct{}{}, }, - }, - HostConfig: &dc.HostConfig{ - Binds: []string{}, - }, - }, nil - }, - FindImageFunc: func(repoTag string) (*dc.Image, error) { - return &dc.Image{Config: &dc.Config{ - Volumes: map[string]struct{}{ - "/var/log": struct{}{}, - }, - }}, nil - }, - } - - container := &container.Container{ - Force: true, - Name: "nginx", - Volumes: []string{"/var/log:/var/log", "/var/db:/var/db:ro"}, - } - container.SetClient(c) - - status, err := container.Check(context.Background(), fakerenderer.New()) - assert.NoError(t, err) - assert.True(t, status.HasChanges()) - comparison.AssertDiff(t, status.Diffs(), "volumes", "/var/log", "/var/db, /var/log") - comparison.AssertDiff(t, status.Diffs(), "binds", "", "/var/db:/var/db:ro, /var/log:/var/log") -} - -func TestContainerCheckVolumesFromNeedsChange(t *testing.T) { - t.Parallel() - - c := &fakeAPIClient{ - FindContainerFunc: func(name string) (*dc.Container, error) { - return &dc.Container{ - Name: name, - Config: &dc.Config{}, - HostConfig: &dc.HostConfig{ - VolumesFrom: []string{}, - }, - }, nil - }, - FindImageFunc: func(repoTag string) (*dc.Image, error) { - return &dc.Image{Config: &dc.Config{}}, nil - }, - } - - container := &container.Container{ - Force: true, - Name: "nginx", - VolumesFrom: []string{"dbvol", "webvol:ro,z"}, - } - container.SetClient(c) - - status, err := container.Check(context.Background(), fakerenderer.New()) - assert.NoError(t, err) - assert.True(t, status.HasChanges()) - comparison.AssertDiff(t, status.Diffs(), "volumes_from", "", "dbvol, webvol:ro,z") + }}, nil + }, + } + + container := &container.Container{ + Force: true, + Name: "nginx", + Volumes: []string{"/var/log:/var/log", "/var/db:/var/db:ro"}, + } + container.SetClient(c) + + status, err := container.Check(context.Background(), fakerenderer.New()) + assert.NoError(t, err) + assert.True(t, status.HasChanges()) + comparison.AssertDiff(t, status.Diffs(), "volumes", "/var/log", "/var/db, /var/log") + comparison.AssertDiff(t, status.Diffs(), "binds", "", "/var/db:/var/db:ro, /var/log:/var/log") + }) + + t.Run("volumes from change", func(t *testing.T) { + c := &fakeAPIClient{ + FindContainerFunc: func(name string) (*dc.Container, error) { + return &dc.Container{ + Name: name, + Config: &dc.Config{}, + HostConfig: &dc.HostConfig{ + VolumesFrom: []string{}, + }, + }, nil + }, + FindImageFunc: func(repoTag string) (*dc.Image, error) { + return &dc.Image{Config: &dc.Config{}}, nil + }, + } + + container := &container.Container{ + Force: true, + Name: "nginx", + VolumesFrom: []string{"dbvol", "webvol:ro,z"}, + } + container.SetClient(c) + + status, err := container.Check(context.Background(), fakerenderer.New()) + assert.NoError(t, err) + assert.True(t, status.HasChanges()) + comparison.AssertDiff(t, status.Diffs(), "volumes_from", "", "dbvol, webvol:ro,z") + }) + + t.Run("network mode change", func(t *testing.T) { + c := &fakeAPIClient{ + FindContainerFunc: func(name string) (*dc.Container, error) { + return &dc.Container{ + Name: name, + Config: &dc.Config{}, + HostConfig: &dc.HostConfig{ + NetworkMode: "bridge", + }, + }, nil + }, + FindImageFunc: func(repoTag string) (*dc.Image, error) { + return &dc.Image{Config: &dc.Config{}}, nil + }, + } + + container := &container.Container{ + Force: true, + Name: "nginx", + NetworkMode: "host", + } + container.SetClient(c) + + status, err := container.Check(fakerenderer.New()) + assert.NoError(t, err) + assert.True(t, status.HasChanges()) + comparison.AssertDiff(t, status.Diffs(), "network_mode", "bridge", "host") + }) + + t.Run("networks change", func(t *testing.T) { + c := &fakeAPIClient{ + FindContainerFunc: func(name string) (*dc.Container, error) { + return &dc.Container{ + Name: name, + Config: &dc.Config{}, + HostConfig: &dc.HostConfig{}, + NetworkSettings: &dc.NetworkSettings{ + Networks: map[string]dc.ContainerNetwork{ + "my-network": dc.ContainerNetwork{}, + }, + }, + }, nil + }, + FindImageFunc: func(repoTag string) (*dc.Image, error) { + return &dc.Image{Config: &dc.Config{}}, nil + }, + } + + container := &container.Container{ + Force: true, + Name: "nginx", + Networks: []string{"test-network", "another-network"}, + } + container.SetClient(c) + + status, err := container.Check(fakerenderer.New()) + assert.NoError(t, err) + assert.True(t, status.HasChanges()) + comparison.AssertDiff(t, status.Diffs(), "networks", "my-network", "another-network, test-network") + }) } +// TestContainerApply tests the Container.Apply function func TestContainerApply(t *testing.T) { t.Parallel() + defer logging.HideLogs(t)() c := &fakeAPIClient{ CreateContainerFunc: func(opts dc.CreateContainerOptions) (*dc.Container, error) { @@ -649,6 +683,7 @@ type fakeAPIClient struct { FindContainerFunc func(name string) (*dc.Container, error) CreateContainerFunc func(opts dc.CreateContainerOptions) (*dc.Container, error) StartContainerFunc func(name, id string) error + ConnectNetworkFunc func(name string, container *dc.Container) error } func (f *fakeAPIClient) FindImage(repoTag string) (*dc.Image, error) { @@ -670,3 +705,7 @@ func (f *fakeAPIClient) CreateContainer(opts dc.CreateContainerOptions) (*dc.Con func (f *fakeAPIClient) StartContainer(name, id string) error { return f.StartContainerFunc(name, id) } + +func (f *fakeAPIClient) ConnectNetwork(name string, container *dc.Container) error { + return f.ConnectNetworkFunc(name, container) +} From e879f0f0412e3300f00f38ac79d1edd09f0107f6 Mon Sep 17 00:00:00 2001 From: ryane Date: Fri, 4 Nov 2016 10:20:03 -0400 Subject: [PATCH 08/14] docker.container: add network mode support --- resource/docker/container/container.go | 11 +++++++++++ resource/docker/container/preparer.go | 7 +++++++ samples/dockerContainer.hcl | 2 ++ 3 files changed, 20 insertions(+) diff --git a/resource/docker/container/container.go b/resource/docker/container/container.go index 9e55d7e28..a5edbecf6 100644 --- a/resource/docker/container/container.go +++ b/resource/docker/container/container.go @@ -32,6 +32,9 @@ import ( const ( containerStatusRunning = "running" containerStatusCreated = "created" + + // DefaultNetworkMode is the mode of the container network + DefaultNetworkMode = "default" ) // these variable names can be injected by the docker engine @@ -54,6 +57,7 @@ type Container struct { Volumes []string VolumesFrom []string PublishAllPorts bool + NetworkMode string CStatus string Force bool client docker.APIClient @@ -105,6 +109,7 @@ func (c *Container) Apply(context.Context) (resource.TaskStatus, error) { PortBindings: toPortBindingMap(c.PortBindings), Binds: binds, VolumesFrom: c.VolumesFrom, + NetworkMode: c.NetworkMode, } opts := dc.CreateContainerOptions{ @@ -156,6 +161,12 @@ func (c *Container) diffContainer(container *dc.Container, status *resource.Stat strings.Join(container.HostConfig.VolumesFrom, ", "), strings.Join(c.VolumesFrom, ", "), "") + status.AddDifference( + "network_mode", + container.HostConfig.NetworkMode, + c.NetworkMode, + DefaultNetworkMode, + ) } image, err := c.client.FindImage(container.Image) diff --git a/resource/docker/container/preparer.go b/resource/docker/container/preparer.go index 4567146b6..75a40fded 100644 --- a/resource/docker/container/preparer.go +++ b/resource/docker/container/preparer.go @@ -66,6 +66,9 @@ type Preparer struct { // list of DNS servers for the container to use DNS []string `hcl:"dns"` + // the mode of the container network + NetworkMode string `hcl:"network_mode"` + // bind mounts volumes Volumes []string `hcl:"volumes"` @@ -110,6 +113,7 @@ func (p *Preparer) Prepare(ctx context.Context, render resource.Renderer) (resou Env: env, Expose: p.Expose, Links: p.Links, + NetworkMode: p.NetworkMode, PublishAllPorts: p.PublishAllPorts, PortBindings: p.Ports, DNS: p.DNS, @@ -133,6 +137,9 @@ func validateContainer(container *Container) error { return errors.New("status must be 'running' or 'created'") } } + if container.NetworkMode == "" { + container.NetworkMode = DefaultNetworkMode + } return nil } diff --git a/samples/dockerContainer.hcl b/samples/dockerContainer.hcl index fb6eee8e3..838120eec 100644 --- a/samples/dockerContainer.hcl +++ b/samples/dockerContainer.hcl @@ -3,6 +3,8 @@ docker.container "nginx" { image = "nginx:1.10-alpine" force = "true" + network_mode = "bridge" + ports = [ "80", ] From e78e2b8b37b99f0df55b09cdd89d63b439873cd5 Mon Sep 17 00:00:00 2001 From: ryane Date: Fri, 4 Nov 2016 14:05:19 -0400 Subject: [PATCH 09/14] docker.container: networks support --- resource/docker/container/container.go | 44 +++++++++++++++++++++++++- resource/docker/container/preparer.go | 4 +++ resource/docker/docker.go | 15 +++++++++ resource/docker/image/image_test.go | 5 +++ resource/docker/network/preparer.go | 4 +++ samples/docker.hcl | 12 +++++-- samples/dockerContainer.hcl | 1 + 7 files changed, 82 insertions(+), 3 deletions(-) diff --git a/resource/docker/container/container.go b/resource/docker/container/container.go index a5edbecf6..9cccd3400 100644 --- a/resource/docker/container/container.go +++ b/resource/docker/container/container.go @@ -38,7 +38,10 @@ const ( ) // these variable names can be injected by the docker engine -var engineEnvVars = []string{"https_proxy", "http_proxy", "no_proxy", "ftp_proxy"} +var ( + engineEnvVars = []string{"https_proxy", "http_proxy", "no_proxy", "ftp_proxy"} + builtinNetworks = []string{"default", "bridge", "host", "none", "container"} +) // Container is responsible for creating docker containers type Container struct { @@ -58,6 +61,7 @@ type Container struct { VolumesFrom []string PublishAllPorts bool NetworkMode string + Networks []string CStatus string Force bool client docker.APIClient @@ -123,6 +127,13 @@ func (c *Container) Apply(context.Context) (resource.TaskStatus, error) { return c, err } + for _, name := range c.Networks { + err = c.client.ConnectNetwork(name, container) + if err != nil { + return c, err + } + } + if c.CStatus == "" || c.CStatus == containerStatusRunning { err = c.client.StartContainer(c.Name, container.ID) if err != nil { @@ -227,6 +238,10 @@ func (c *Container) diffContainer(container *dc.Container, status *resource.Stat actual, expected = c.compareBinds(container) status.AddDifference("binds", actual, expected, "") + // Networks + actual, expected = c.compareNetworks(container) + status.AddDifference("networks", actual, expected, "") + // Image existingRepoTag := preferredRepoTag(c.Image, image) status.AddDifference("image", existingRepoTag, c.Image, "") @@ -234,6 +249,33 @@ func (c *Container) diffContainer(container *dc.Container, status *resource.Stat return nil } +func (c *Container) compareNetworks(container *dc.Container) (actual, expected string) { + if container.NetworkSettings == nil { + return "", "" + } + + var containerNetworks []string + for name := range container.NetworkSettings.Networks { + var isBuiltin bool + for _, builtin := range builtinNetworks { + if strings.EqualFold(name, builtin) { + isBuiltin = true + break + } + } + if !isBuiltin { + containerNetworks = append(containerNetworks, name) + } + } + sort.Strings(containerNetworks) + + expectedNetworks := make([]string, len(c.Networks)) + copy(expectedNetworks, c.Networks) + sort.Strings(expectedNetworks) + + return strings.Join(containerNetworks, ", "), strings.Join(expectedNetworks, ", ") +} + func (c *Container) compareEnv(container *dc.Container, image *dc.Image) (actual, expected string) { varName := func(envvar string) string { return strings.ToLower(strings.Split(envvar, "=")[0]) diff --git a/resource/docker/container/preparer.go b/resource/docker/container/preparer.go index 75a40fded..32f583b18 100644 --- a/resource/docker/container/preparer.go +++ b/resource/docker/container/preparer.go @@ -69,6 +69,9 @@ type Preparer struct { // the mode of the container network NetworkMode string `hcl:"network_mode"` + // the networks to connect the container to + Networks []string `hcl:"networks"` + // bind mounts volumes Volumes []string `hcl:"volumes"` @@ -114,6 +117,7 @@ func (p *Preparer) Prepare(ctx context.Context, render resource.Renderer) (resou Expose: p.Expose, Links: p.Links, NetworkMode: p.NetworkMode, + Networks: p.Networks, PublishAllPorts: p.PublishAllPorts, PortBindings: p.Ports, DNS: p.DNS, diff --git a/resource/docker/docker.go b/resource/docker/docker.go index e1fdccc39..90b63abe3 100644 --- a/resource/docker/docker.go +++ b/resource/docker/docker.go @@ -30,6 +30,7 @@ type APIClient interface { FindContainer(string) (*dc.Container, error) CreateContainer(dc.CreateContainerOptions) (*dc.Container, error) StartContainer(string, string) error + ConnectNetwork(string, *dc.Container) error } // VolumeClient manages Docker volumes @@ -265,6 +266,20 @@ func (c *Client) FindVolume(name string) (*dc.Volume, error) { return volume, nil } +// ConnectNetwork connects the container to a network +func (c *Client) ConnectNetwork(name string, container *dc.Container) error { + log.WithField("module", "docker").WithFields( + log.Fields{"name": container.Name, "network": name, "id": container.ID}, + ).Debug("connecting to network") + + err := c.Client.ConnectNetwork(name, dc.NetworkConnectionOptions{Container: container.ID}) + if err != nil { + err = errors.Wrapf(err, "failed to connect container %s (%s) to network %s", container.Name, container.ID, name) + } + + return err +} + // FindNetwork finds the network with the specified name func (c *Client) FindNetwork(name string) (*dc.Network, error) { networks, err := c.Client.FilteredListNetworks(dc.NetworkFilterOpts{ diff --git a/resource/docker/image/image_test.go b/resource/docker/image/image_test.go index 2714e38db..89de6dfb8 100644 --- a/resource/docker/image/image_test.go +++ b/resource/docker/image/image_test.go @@ -144,6 +144,7 @@ type fakeAPIClient struct { FindContainerFunc func(name string) (*dc.Container, error) CreateContainerFunc func(opts dc.CreateContainerOptions) (*dc.Container, error) StartContainerFunc func(name, id string) error + ConnectNetworkFunc func(name string, container *dc.Container) error } func (f *fakeAPIClient) FindImage(repoTag string) (*dc.Image, error) { @@ -165,3 +166,7 @@ func (f *fakeAPIClient) CreateContainer(opts dc.CreateContainerOptions) (*dc.Con func (f *fakeAPIClient) StartContainer(name, id string) error { return f.StartContainerFunc(name, id) } + +func (f *fakeAPIClient) ConnectNetwork(name string, container *dc.Container) error { + return f.ConnectNetworkFunc(name, container) +} diff --git a/resource/docker/network/preparer.go b/resource/docker/network/preparer.go index e34ce0e93..27e79cccf 100644 --- a/resource/docker/network/preparer.go +++ b/resource/docker/network/preparer.go @@ -79,6 +79,10 @@ func (p *Preparer) Prepare(render resource.Renderer) (resource.Task, error) { p.Driver = DefaultDriver } + if p.IPAMDriver == "" { + p.IPAMDriver = DefaultIPAMDriver + } + if p.State == "" { p.State = "present" } diff --git a/samples/docker.hcl b/samples/docker.hcl index 4e67abd31..f467111df 100644 --- a/samples/docker.hcl +++ b/samples/docker.hcl @@ -20,10 +20,16 @@ docker.volume "nginx-content" { name = "nginx-html" } +docker.network "nginx-network" { + name = "nginx-network" + state = "present" + force = true +} + docker.container "nginx" { name = "{{param `container`}}" image = "{{lookup `docker.image.nginx.Name`}}:{{lookup `docker.image.nginx.Tag`}}" - force = "true" + force = true expose = [ "80", @@ -31,7 +37,7 @@ docker.container "nginx" { "8080", ] - publish_all_ports = "false" + publish_all_ports = false ports = [ "80", @@ -45,5 +51,7 @@ docker.container "nginx" { "FOO" = "BAR" } + networks = ["{{lookup `docker.network.nginx-network.name`}}"] + dns = ["8.8.8.8", "8.8.4.4"] } diff --git a/samples/dockerContainer.hcl b/samples/dockerContainer.hcl index 838120eec..bb6e4fda1 100644 --- a/samples/dockerContainer.hcl +++ b/samples/dockerContainer.hcl @@ -4,6 +4,7 @@ docker.container "nginx" { force = "true" network_mode = "bridge" + networks = ["test-network", "test"] ports = [ "80", From f659660b87106aa6ce60991a26ce2abcc5a4a411 Mon Sep 17 00:00:00 2001 From: ryane Date: Fri, 4 Nov 2016 15:30:43 -0400 Subject: [PATCH 10/14] docker.network: add context --- resource/docker/container/container_test.go | 4 +- resource/docker/network/network.go | 5 ++- resource/docker/network/network_test.go | 41 +++++++++++---------- resource/docker/network/preparer.go | 3 +- resource/docker/network/preparer_test.go | 7 ++-- 5 files changed, 32 insertions(+), 28 deletions(-) diff --git a/resource/docker/container/container_test.go b/resource/docker/container/container_test.go index 28c677bf3..87b9028d5 100644 --- a/resource/docker/container/container_test.go +++ b/resource/docker/container/container_test.go @@ -620,7 +620,7 @@ func TestContainerCheck(t *testing.T) { } container.SetClient(c) - status, err := container.Check(fakerenderer.New()) + status, err := container.Check(context.Background(), fakerenderer.New()) assert.NoError(t, err) assert.True(t, status.HasChanges()) comparison.AssertDiff(t, status.Diffs(), "network_mode", "bridge", "host") @@ -652,7 +652,7 @@ func TestContainerCheck(t *testing.T) { } container.SetClient(c) - status, err := container.Check(fakerenderer.New()) + status, err := container.Check(context.Background(), fakerenderer.New()) assert.NoError(t, err) assert.True(t, status.HasChanges()) comparison.AssertDiff(t, status.Diffs(), "networks", "my-network", "another-network, test-network") diff --git a/resource/docker/network/network.go b/resource/docker/network/network.go index 90001f3c1..22aa4e1db 100644 --- a/resource/docker/network/network.go +++ b/resource/docker/network/network.go @@ -24,6 +24,7 @@ import ( "github.com/asteris-llc/converge/resource" "github.com/asteris-llc/converge/resource/docker" dc "github.com/fsouza/go-dockerclient" + "golang.org/x/net/context" ) // State type for Network @@ -60,7 +61,7 @@ type Network struct { } // Check system for docker network -func (n *Network) Check(resource.Renderer) (resource.TaskStatus, error) { +func (n *Network) Check(context.Context, resource.Renderer) (resource.TaskStatus, error) { n.Status = resource.NewStatus() nw, err := n.client.FindNetwork(n.Name) @@ -83,7 +84,7 @@ func (n *Network) Check(resource.Renderer) (resource.TaskStatus, error) { } // Apply ensures the network matches the desired state -func (n *Network) Apply() (resource.TaskStatus, error) { +func (n *Network) Apply(context.Context) (resource.TaskStatus, error) { n.Status = resource.NewStatus() var ( diff --git a/resource/docker/network/network_test.go b/resource/docker/network/network_test.go index a494a5bc6..b811a0073 100644 --- a/resource/docker/network/network_test.go +++ b/resource/docker/network/network_test.go @@ -27,6 +27,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "golang.org/x/net/context" ) // TestNetworkInterface verifies that Network implements the resource.Task @@ -52,7 +53,7 @@ func TestNetworkCheck(t *testing.T) { nw.SetClient(c) c.On("FindNetwork", nwName).Return(nil, nil) - status, err := nw.Check(fakerenderer.New()) + status, err := nw.Check(context.Background(), fakerenderer.New()) require.NoError(t, err) assert.False(t, status.HasChanges()) assert.Equal(t, 1, len(status.Diffs())) @@ -65,7 +66,7 @@ func TestNetworkCheck(t *testing.T) { nw.SetClient(c) c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName}, nil) - status, err := nw.Check(fakerenderer.New()) + status, err := nw.Check(context.Background(), fakerenderer.New()) require.NoError(t, err) assert.True(t, status.HasChanges()) assert.Equal(t, 1, len(status.Diffs())) @@ -80,7 +81,7 @@ func TestNetworkCheck(t *testing.T) { nw.SetClient(c) c.On("FindNetwork", nwName).Return(nil, nil) - status, err := nw.Check(fakerenderer.New()) + status, err := nw.Check(context.Background(), fakerenderer.New()) require.NoError(t, err) assert.True(t, status.HasChanges()) assert.Equal(t, 1, len(status.Diffs())) @@ -93,7 +94,7 @@ func TestNetworkCheck(t *testing.T) { nw.SetClient(c) c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName}, nil) - status, err := nw.Check(fakerenderer.New()) + status, err := nw.Check(context.Background(), fakerenderer.New()) require.NoError(t, err) assert.False(t, status.HasChanges()) assert.Equal(t, 1, len(status.Diffs())) @@ -112,7 +113,7 @@ func TestNetworkCheck(t *testing.T) { nw.SetClient(c) c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName}, nil) - status, err := nw.Check(fakerenderer.New()) + status, err := nw.Check(context.Background(), fakerenderer.New()) require.NoError(t, err) assert.True(t, status.HasChanges()) assert.True(t, len(status.Diffs()) > 1) @@ -134,7 +135,7 @@ func TestNetworkCheck(t *testing.T) { Driver: network.DefaultDriver, }, nil) - status, err := nw.Check(fakerenderer.New()) + status, err := nw.Check(context.Background(), fakerenderer.New()) require.NoError(t, err) assert.True(t, status.HasChanges()) assert.True(t, len(status.Diffs()) > 1) @@ -156,7 +157,7 @@ func TestNetworkCheck(t *testing.T) { Options: nil, }, nil) - status, err := nw.Check(fakerenderer.New()) + status, err := nw.Check(context.Background(), fakerenderer.New()) require.NoError(t, err) assert.True(t, status.HasChanges()) assert.True(t, len(status.Diffs()) > 1) @@ -182,7 +183,7 @@ func TestNetworkCheck(t *testing.T) { Name: nwName, }, nil) - status, err := nw.Check(fakerenderer.New()) + status, err := nw.Check(context.Background(), fakerenderer.New()) require.NoError(t, err) assert.True(t, status.HasChanges()) assert.True(t, len(status.Diffs()) > 1) @@ -204,7 +205,7 @@ func TestNetworkCheck(t *testing.T) { Internal: false, }, nil) - status, err := nw.Check(fakerenderer.New()) + status, err := nw.Check(context.Background(), fakerenderer.New()) require.NoError(t, err) assert.True(t, status.HasChanges()) assert.True(t, len(status.Diffs()) > 1) @@ -226,7 +227,7 @@ func TestNetworkCheck(t *testing.T) { EnableIPv6: false, }, nil) - status, err := nw.Check(fakerenderer.New()) + status, err := nw.Check(context.Background(), fakerenderer.New()) require.NoError(t, err) assert.True(t, status.HasChanges()) assert.True(t, len(status.Diffs()) > 1) @@ -242,7 +243,7 @@ func TestNetworkCheck(t *testing.T) { nw.SetClient(c) c.On("FindNetwork", nwName).Return(nil, errors.New("error")) - status, err := nw.Check(fakerenderer.New()) + status, err := nw.Check(context.Background(), fakerenderer.New()) require.Error(t, err) assert.Equal(t, resource.StatusFatal, status.StatusCode()) }) @@ -261,7 +262,7 @@ func TestNetworkApply(t *testing.T) { nw.SetClient(c) c.On("FindNetwork", nwName).Return(nil, errors.New("error")) - status, err := nw.Apply() + status, err := nw.Apply(context.Background()) require.Error(t, err) assert.Equal(t, resource.StatusFatal, status.StatusCode()) }) @@ -275,7 +276,7 @@ func TestNetworkApply(t *testing.T) { c.On("CreateNetwork", mock.AnythingOfType("docker.CreateNetworkOptions")). Return(&dc.Network{Name: nwName}, nil) - status, err := nw.Apply() + status, err := nw.Apply(context.Background()) require.NoError(t, err) assert.True(t, status.HasChanges()) c.AssertCalled(t, "CreateNetwork", mock.AnythingOfType("docker.CreateNetworkOptions")) @@ -288,7 +289,7 @@ func TestNetworkApply(t *testing.T) { nw.SetClient(c) c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName}, nil) - status, err := nw.Apply() + status, err := nw.Apply(context.Background()) require.NoError(t, err) assert.False(t, status.HasChanges()) c.AssertNotCalled(t, "CreateNetwork", mock.AnythingOfType("docker.CreateNetworkOptions")) @@ -304,7 +305,7 @@ func TestNetworkApply(t *testing.T) { c.On("CreateNetwork", mock.AnythingOfType("docker.CreateNetworkOptions")). Return(&dc.Network{Name: nwName}, nil) - status, err := nw.Apply() + status, err := nw.Apply(context.Background()) require.NoError(t, err) assert.True(t, status.HasChanges()) c.AssertCalled(t, "RemoveNetwork", nwName) @@ -319,7 +320,7 @@ func TestNetworkApply(t *testing.T) { c.On("CreateNetwork", mock.AnythingOfType("docker.CreateNetworkOptions")). Return(nil, errors.New("error")) - status, err := nw.Apply() + status, err := nw.Apply(context.Background()) require.Error(t, err) assert.Equal(t, resource.StatusFatal, status.StatusCode()) }) @@ -331,7 +332,7 @@ func TestNetworkApply(t *testing.T) { c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName, Driver: "test"}, nil) c.On("RemoveNetwork", mock.AnythingOfType("string")).Return(errors.New("error")) - status, err := nw.Apply() + status, err := nw.Apply(context.Background()) require.Error(t, err) assert.Equal(t, resource.StatusFatal, status.StatusCode()) }) @@ -345,7 +346,7 @@ func TestNetworkApply(t *testing.T) { c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName}, nil) c.On("RemoveNetwork", nwName).Return(nil) - status, err := nw.Apply() + status, err := nw.Apply(context.Background()) require.NoError(t, err) assert.True(t, status.HasChanges()) c.AssertCalled(t, "RemoveNetwork", nwName) @@ -357,7 +358,7 @@ func TestNetworkApply(t *testing.T) { nw.SetClient(c) c.On("FindNetwork", nwName).Return(nil, nil) - status, err := nw.Apply() + status, err := nw.Apply(context.Background()) require.NoError(t, err) assert.False(t, status.HasChanges()) c.AssertNotCalled(t, "RemoveNetwork", nwName) @@ -370,7 +371,7 @@ func TestNetworkApply(t *testing.T) { c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName}, nil) c.On("RemoveNetwork", nwName).Return(errors.New("error")) - status, err := nw.Apply() + status, err := nw.Apply(context.Background()) require.Error(t, err) assert.Equal(t, resource.StatusFatal, status.StatusCode()) }) diff --git a/resource/docker/network/preparer.go b/resource/docker/network/preparer.go index 27e79cccf..f1d3bab3d 100644 --- a/resource/docker/network/preparer.go +++ b/resource/docker/network/preparer.go @@ -21,6 +21,7 @@ import ( "github.com/asteris-llc/converge/resource" "github.com/asteris-llc/converge/resource/docker" dc "github.com/fsouza/go-dockerclient" + "golang.org/x/net/context" ) // Preparer for docker networks @@ -70,7 +71,7 @@ type Preparer struct { } // Prepare a docker network -func (p *Preparer) Prepare(render resource.Renderer) (resource.Task, error) { +func (p *Preparer) Prepare(ctx context.Context, render resource.Renderer) (resource.Task, error) { if p.Name == "" { return nil, errors.New("name must be provided") } diff --git a/resource/docker/network/preparer_test.go b/resource/docker/network/preparer_test.go index 7efdecc96..e0a22786a 100644 --- a/resource/docker/network/preparer_test.go +++ b/resource/docker/network/preparer_test.go @@ -22,6 +22,7 @@ import ( "github.com/asteris-llc/converge/resource/docker/network" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/net/context" ) // TestPreparerInterface ensures that the correct interfaces are implemented by @@ -35,7 +36,7 @@ func TestPreparerInterface(t *testing.T) { func TestPreparerPrepare(t *testing.T) { t.Run("name is required", func(t *testing.T) { p := &network.Preparer{Name: ""} - _, err := p.Prepare(fakerenderer.New()) + _, err := p.Prepare(context.Background(), fakerenderer.New()) if assert.Error(t, err) { assert.EqualError(t, err, "name must be provided") } @@ -43,7 +44,7 @@ func TestPreparerPrepare(t *testing.T) { t.Run("state defaults to present", func(t *testing.T) { p := &network.Preparer{Name: "test-network"} - task, err := p.Prepare(fakerenderer.New()) + task, err := p.Prepare(context.Background(), fakerenderer.New()) require.NoError(t, err) require.IsType(t, (*network.Network)(nil), task) nw := task.(*network.Network) @@ -52,7 +53,7 @@ func TestPreparerPrepare(t *testing.T) { t.Run("driver defaults to bridge", func(t *testing.T) { p := &network.Preparer{Name: "test-network"} - task, err := p.Prepare(fakerenderer.New()) + task, err := p.Prepare(context.Background(), fakerenderer.New()) require.NoError(t, err) require.IsType(t, (*network.Network)(nil), task) nw := task.(*network.Network) From bf468c83bb41e0f09fdabe9e746f3cd89b773475 Mon Sep 17 00:00:00 2001 From: ryane Date: Tue, 8 Nov 2016 16:22:32 -0500 Subject: [PATCH 11/14] docker.network: validate gateways when creating network --- resource/docker/docker.go | 6 +++ resource/docker/network/network.go | 59 ++++++++++++++++++++++++- resource/docker/network/network_test.go | 43 ++++++++++++++++++ 3 files changed, 106 insertions(+), 2 deletions(-) diff --git a/resource/docker/docker.go b/resource/docker/docker.go index 90b63abe3..bdc7c47ec 100644 --- a/resource/docker/docker.go +++ b/resource/docker/docker.go @@ -48,6 +48,7 @@ type Client struct { // NetworkClient manage Docker networks type NetworkClient interface { + ListNetworks() ([]dc.Network, error) FindNetwork(string) (*dc.Network, error) CreateNetwork(dc.CreateNetworkOptions) (*dc.Network, error) RemoveNetwork(string) error @@ -300,6 +301,11 @@ func (c *Client) FindNetwork(name string) (*dc.Network, error) { return nil, nil } +// ListNetworks returns the docker networks +func (c *Client) ListNetworks() ([]dc.Network, error) { + return c.Client.ListNetworks() +} + // CreateNetwork creates a docker network func (c *Client) CreateNetwork(opts dc.CreateNetworkOptions) (*dc.Network, error) { log.WithFields(log.Fields{ diff --git a/resource/docker/network/network.go b/resource/docker/network/network.go index 22aa4e1db..2aa9512d4 100644 --- a/resource/docker/network/network.go +++ b/resource/docker/network/network.go @@ -72,8 +72,21 @@ func (n *Network) Check(context.Context, resource.Renderer) (resource.TaskStatus n.AddDifference(n.Name, string(networkState(nw)), string(n.State), "") - if n.State == StatePresent && nw != nil && n.Force { - n.diffNetwork(nw) + if n.State == StatePresent { + if nw != nil && n.Force { + n.diffNetwork(nw) + } + + ok, err := n.isCreatable(nw) + if err != nil { + n.RaiseLevel(resource.StatusFatal) + return n, err + } + + if !ok { + n.RaiseLevel(resource.StatusCantChange) + return n, err + } } if resource.AnyChanges(n.Differences) { @@ -170,6 +183,48 @@ func (n *Network) diffNetwork(nw *dc.Network) { } } +// isCreatable can validate whether the network can be created on the docker +// host. use sparingly as behavior can vary across different docker network +// plugins. in most cases, we can rely on the docker api to return errors during +// apply +func (n *Network) isCreatable(nw *dc.Network) (bool, error) { + if len(n.IPAM.Config) > 0 { + inUse, err := n.gatewayInUse(nw) + return !inUse, err + } + + return true, nil +} + +func (n *Network) gatewayInUse(nw *dc.Network) (bool, error) { + var gateways []string + networks, err := n.client.ListNetworks() + if err != nil { + return false, err + } + + for _, network := range networks { + if nw == nil || network.ID != nw.ID { + for _, ipamConfig := range network.IPAM.Config { + if ipamConfig.Gateway != "" { + gateways = append(gateways, ipamConfig.Gateway) + } + } + } + } + + for _, ipamConfig := range n.IPAM.Config { + for _, gateway := range gateways { + if strings.EqualFold(ipamConfig.Gateway, gateway) { + n.Status.AddMessage(fmt.Sprintf("gateway %s already in use", gateway)) + return true, nil + } + } + } + + return false, nil +} + func networkState(nw *dc.Network) State { if nw != nil { return StatePresent diff --git a/resource/docker/network/network_test.go b/resource/docker/network/network_test.go index b811a0073..af3c55cac 100644 --- a/resource/docker/network/network_test.go +++ b/resource/docker/network/network_test.go @@ -79,6 +79,7 @@ func TestNetworkCheck(t *testing.T) { nw := &network.Network{Name: nwName, State: "present"} c := &mockClient{} nw.SetClient(c) + c.On("ListNetworks").Return(nil, nil) c.On("FindNetwork", nwName).Return(nil, nil) status, err := nw.Check(context.Background(), fakerenderer.New()) @@ -92,6 +93,7 @@ func TestNetworkCheck(t *testing.T) { nw := &network.Network{Name: nwName, State: "present"} c := &mockClient{} nw.SetClient(c) + c.On("ListNetworks").Return(nil, nil) c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName}, nil) status, err := nw.Check(context.Background(), fakerenderer.New()) @@ -101,6 +103,32 @@ func TestNetworkCheck(t *testing.T) { comparison.AssertDiff(t, status.Diffs(), nwName, "present", "present") }) + t.Run("gateway conflicts", func(t *testing.T) { + nw := &network.Network{ + Name: nwName, + State: "present", + IPAM: dc.IPAMOptions{ + Config: []dc.IPAMConfig{ + dc.IPAMConfig{Gateway: "192.168.1.1"}, + }, + }, + } + c := &mockClient{} + nw.SetClient(c) + c.On("ListNetworks").Return([]dc.Network{ + dc.Network{ID: "123", IPAM: dc.IPAMOptions{ + Config: []dc.IPAMConfig{ + dc.IPAMConfig{Gateway: "192.168.1.1"}, + }, + }}, + }, nil) + c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName}, nil) + + status, err := nw.Check(context.Background(), fakerenderer.New()) + require.NoError(t, err) + assert.Equal(t, resource.StatusCantChange, status.StatusCode()) + }) + t.Run("network exists, force: true", func(t *testing.T) { t.Run("labels", func(t *testing.T) { nw := &network.Network{ @@ -112,6 +140,7 @@ func TestNetworkCheck(t *testing.T) { c := &mockClient{} nw.SetClient(c) c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName}, nil) + c.On("ListNetworks").Return(nil, nil) status, err := nw.Check(context.Background(), fakerenderer.New()) require.NoError(t, err) @@ -134,6 +163,7 @@ func TestNetworkCheck(t *testing.T) { Name: nwName, Driver: network.DefaultDriver, }, nil) + c.On("ListNetworks").Return(nil, nil) status, err := nw.Check(context.Background(), fakerenderer.New()) require.NoError(t, err) @@ -156,6 +186,7 @@ func TestNetworkCheck(t *testing.T) { Name: nwName, Options: nil, }, nil) + c.On("ListNetworks").Return(nil, nil) status, err := nw.Check(context.Background(), fakerenderer.New()) require.NoError(t, err) @@ -182,6 +213,7 @@ func TestNetworkCheck(t *testing.T) { c.On("FindNetwork", nwName).Return(&dc.Network{ Name: nwName, }, nil) + c.On("ListNetworks").Return(nil, nil) status, err := nw.Check(context.Background(), fakerenderer.New()) require.NoError(t, err) @@ -204,6 +236,7 @@ func TestNetworkCheck(t *testing.T) { Name: nwName, Internal: false, }, nil) + c.On("ListNetworks").Return(nil, nil) status, err := nw.Check(context.Background(), fakerenderer.New()) require.NoError(t, err) @@ -226,6 +259,7 @@ func TestNetworkCheck(t *testing.T) { Name: nwName, EnableIPv6: false, }, nil) + c.On("ListNetworks").Return(nil, nil) status, err := nw.Check(context.Background(), fakerenderer.New()) require.NoError(t, err) @@ -382,6 +416,15 @@ type mockClient struct { mock.Mock } +func (m *mockClient) ListNetworks() ([]dc.Network, error) { + args := m.Called() + ret := args.Get(0) + if ret == nil { + return nil, args.Error(1) + } + return ret.([]dc.Network), args.Error(1) +} + func (m *mockClient) FindNetwork(name string) (*dc.Network, error) { args := m.Called(name) ret := args.Get(0) From f227bf12939f9ff5d06ee63f53004f5861f9e86c Mon Sep 17 00:00:00 2001 From: ryane Date: Tue, 8 Nov 2016 16:25:03 -0500 Subject: [PATCH 12/14] dockerContainer.hcl: should plan/apply standalone --- samples/dockerContainer.hcl | 3 --- 1 file changed, 3 deletions(-) diff --git a/samples/dockerContainer.hcl b/samples/dockerContainer.hcl index bb6e4fda1..fb6eee8e3 100644 --- a/samples/dockerContainer.hcl +++ b/samples/dockerContainer.hcl @@ -3,9 +3,6 @@ docker.container "nginx" { image = "nginx:1.10-alpine" force = "true" - network_mode = "bridge" - networks = ["test-network", "test"] - ports = [ "80", ] From fe6a2a95c28be7c2e6b56ea958dcade93b01ed88 Mon Sep 17 00:00:00 2001 From: ryane Date: Tue, 8 Nov 2016 16:55:45 -0500 Subject: [PATCH 13/14] docker.network: fix some minor doc issues --- docs/content/resources/docker.container.md | 8 ++++++++ docs/content/resources/docker.network.md | 13 +++++++------ resource/docker/container/preparer.go | 2 +- resource/docker/network/network.go | 2 +- resource/docker/network/preparer.go | 15 ++++++++------- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/docs/content/resources/docker.container.md b/docs/content/resources/docker.container.md index 4f9172e11..7343a5733 100644 --- a/docs/content/resources/docker.container.md +++ b/docs/content/resources/docker.container.md @@ -79,6 +79,14 @@ specified, "tcp" is assumed list of DNS servers for the container to use +- `network_mode` (string) + + the mode of the container network. default: default + +- `networks` (list of strings) + + the networks to connect the container to + - `volumes` (list of strings) bind mounts volumes diff --git a/docs/content/resources/docker.network.md b/docs/content/resources/docker.network.md index 6c20f626f..c095a4bd1 100644 --- a/docs/content/resources/docker.network.md +++ b/docs/content/resources/docker.network.md @@ -66,12 +66,13 @@ docker.network "test-network" { - `ipam_driver` (string) - ip address management driver + ip address management driver. default: default - `ipam_config` (list of ipamConfigMaps) - custom IPAM configuration. multiple IPAM configurations are permitted. Each -IPAM configuration block should contain one or more of the following items: + optional custom IPAM configuration. multiple IPAM configurations are +permitted. Each IPAM configuration block should contain one or more of the +following items: * subnet: subnet in CIDR format * gateway: ipv4 or ipv6 gateway for the corresponding subnet @@ -93,12 +94,12 @@ IPAM configuration block should contain one or more of the following items: Valid values: `present` and `absent` - indicates whether the volume should exist. + indicates whether the network should exist. default: present - `force` (bool) - indicates whether or not the volume will be recreated if the state is not + indicates whether or not the network will be recreated if the state is not what is expected. By default, the module will only check to see if the -volume exists. Specified as a boolean value +network exists. Specified as a boolean value diff --git a/resource/docker/container/preparer.go b/resource/docker/container/preparer.go index 32f583b18..be5e27da2 100644 --- a/resource/docker/container/preparer.go +++ b/resource/docker/container/preparer.go @@ -66,7 +66,7 @@ type Preparer struct { // list of DNS servers for the container to use DNS []string `hcl:"dns"` - // the mode of the container network + // the mode of the container network. default: default NetworkMode string `hcl:"network_mode"` // the networks to connect the container to diff --git a/resource/docker/network/network.go b/resource/docker/network/network.go index 2aa9512d4..4952f499a 100644 --- a/resource/docker/network/network.go +++ b/resource/docker/network/network.go @@ -44,7 +44,7 @@ const ( DefaultIPAMDriver = "default" ) -// Network is responsible for managing docker volumes +// Network is responsible for managing docker networks type Network struct { *resource.Status client docker.NetworkClient diff --git a/resource/docker/network/preparer.go b/resource/docker/network/preparer.go index f1d3bab3d..9c4bcf677 100644 --- a/resource/docker/network/preparer.go +++ b/resource/docker/network/preparer.go @@ -41,11 +41,12 @@ type Preparer struct { // driver specific options Options map[string]interface{} `hcl:"options"` - // ip address management driver + // ip address management driver. default: default IPAMDriver string `hcl:"ipam_driver"` - // custom IPAM configuration. multiple IPAM configurations are permitted. Each - // IPAM configuration block should contain one or more of the following items: + // optional custom IPAM configuration. multiple IPAM configurations are + // permitted. Each IPAM configuration block should contain one or more of the + // following items: // // * subnet: subnet in CIDR format // * gateway: ipv4 or ipv6 gateway for the corresponding subnet @@ -61,12 +62,12 @@ type Preparer struct { // enable ipv6 networking IPv6 bool `hcl:"ipv6"` - // indicates whether the volume should exist. + // indicates whether the network should exist. default: present State State `hcl:"state" valid_values:"present,absent"` - // indicates whether or not the volume will be recreated if the state is not + // indicates whether or not the network will be recreated if the state is not // what is expected. By default, the module will only check to see if the - // volume exists. Specified as a boolean value + // network exists. Specified as a boolean value Force bool `hcl:"force"` } @@ -85,7 +86,7 @@ func (p *Preparer) Prepare(ctx context.Context, render resource.Renderer) (resou } if p.State == "" { - p.State = "present" + p.State = StatePresent } dockerClient, err := docker.NewDockerClient() From ebaf72a02015da3769e6ebafb027da88ddd94118 Mon Sep 17 00:00:00 2001 From: ryane Date: Wed, 9 Nov 2016 10:03:34 -0500 Subject: [PATCH 14/14] docker.network: use State* in tests --- resource/docker/network/network_test.go | 60 ++++++++++++------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/resource/docker/network/network_test.go b/resource/docker/network/network_test.go index af3c55cac..43383e3a3 100644 --- a/resource/docker/network/network_test.go +++ b/resource/docker/network/network_test.go @@ -48,7 +48,7 @@ func TestNetworkCheck(t *testing.T) { t.Run("state: absent", func(t *testing.T) { t.Run("network does not exist", func(t *testing.T) { - nw := &network.Network{Name: nwName, State: "absent"} + nw := &network.Network{Name: nwName, State: network.StateAbsent} c := &mockClient{} nw.SetClient(c) c.On("FindNetwork", nwName).Return(nil, nil) @@ -57,11 +57,11 @@ func TestNetworkCheck(t *testing.T) { require.NoError(t, err) assert.False(t, status.HasChanges()) assert.Equal(t, 1, len(status.Diffs())) - comparison.AssertDiff(t, status.Diffs(), nwName, "absent", "absent") + comparison.AssertDiff(t, status.Diffs(), nwName, string(network.StateAbsent), string(network.StateAbsent)) }) t.Run("network exists", func(t *testing.T) { - nw := &network.Network{Name: nwName, State: "absent"} + nw := &network.Network{Name: nwName, State: network.StateAbsent} c := &mockClient{} nw.SetClient(c) c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName}, nil) @@ -70,13 +70,13 @@ func TestNetworkCheck(t *testing.T) { require.NoError(t, err) assert.True(t, status.HasChanges()) assert.Equal(t, 1, len(status.Diffs())) - comparison.AssertDiff(t, status.Diffs(), nwName, "present", "absent") + comparison.AssertDiff(t, status.Diffs(), nwName, string(network.StatePresent), string(network.StateAbsent)) }) }) t.Run("state: present", func(t *testing.T) { t.Run("network does not exist", func(t *testing.T) { - nw := &network.Network{Name: nwName, State: "present"} + nw := &network.Network{Name: nwName, State: network.StatePresent} c := &mockClient{} nw.SetClient(c) c.On("ListNetworks").Return(nil, nil) @@ -86,11 +86,11 @@ func TestNetworkCheck(t *testing.T) { require.NoError(t, err) assert.True(t, status.HasChanges()) assert.Equal(t, 1, len(status.Diffs())) - comparison.AssertDiff(t, status.Diffs(), nwName, "absent", "present") + comparison.AssertDiff(t, status.Diffs(), nwName, string(network.StateAbsent), string(network.StatePresent)) }) t.Run("network exists", func(t *testing.T) { - nw := &network.Network{Name: nwName, State: "present"} + nw := &network.Network{Name: nwName, State: network.StatePresent} c := &mockClient{} nw.SetClient(c) c.On("ListNetworks").Return(nil, nil) @@ -100,13 +100,13 @@ func TestNetworkCheck(t *testing.T) { require.NoError(t, err) assert.False(t, status.HasChanges()) assert.Equal(t, 1, len(status.Diffs())) - comparison.AssertDiff(t, status.Diffs(), nwName, "present", "present") + comparison.AssertDiff(t, status.Diffs(), nwName, string(network.StatePresent), string(network.StatePresent)) }) t.Run("gateway conflicts", func(t *testing.T) { nw := &network.Network{ Name: nwName, - State: "present", + State: network.StatePresent, IPAM: dc.IPAMOptions{ Config: []dc.IPAMConfig{ dc.IPAMConfig{Gateway: "192.168.1.1"}, @@ -133,7 +133,7 @@ func TestNetworkCheck(t *testing.T) { t.Run("labels", func(t *testing.T) { nw := &network.Network{ Name: nwName, - State: "present", + State: network.StatePresent, Labels: map[string]string{"key": "val", "test": "val2"}, Force: true, } @@ -146,14 +146,14 @@ func TestNetworkCheck(t *testing.T) { require.NoError(t, err) assert.True(t, status.HasChanges()) assert.True(t, len(status.Diffs()) > 1) - comparison.AssertDiff(t, status.Diffs(), nwName, "present", "present") + comparison.AssertDiff(t, status.Diffs(), nwName, string(network.StatePresent), string(network.StatePresent)) comparison.AssertDiff(t, status.Diffs(), "labels", "", "key=val, test=val2") }) t.Run("driver", func(t *testing.T) { nw := &network.Network{ Name: nwName, - State: "present", + State: network.StatePresent, Driver: "weave", Force: true, } @@ -169,14 +169,14 @@ func TestNetworkCheck(t *testing.T) { require.NoError(t, err) assert.True(t, status.HasChanges()) assert.True(t, len(status.Diffs()) > 1) - comparison.AssertDiff(t, status.Diffs(), nwName, "present", "present") + comparison.AssertDiff(t, status.Diffs(), nwName, string(network.StatePresent), string(network.StatePresent)) comparison.AssertDiff(t, status.Diffs(), "driver", network.DefaultDriver, "weave") }) t.Run("options", func(t *testing.T) { nw := &network.Network{ Name: nwName, - State: "present", + State: network.StatePresent, Options: map[string]interface{}{"com.docker.network.bridge.enable_icc": "true"}, Force: true, } @@ -192,14 +192,14 @@ func TestNetworkCheck(t *testing.T) { require.NoError(t, err) assert.True(t, status.HasChanges()) assert.True(t, len(status.Diffs()) > 1) - comparison.AssertDiff(t, status.Diffs(), nwName, "present", "present") + comparison.AssertDiff(t, status.Diffs(), nwName, string(network.StatePresent), string(network.StatePresent)) comparison.AssertDiff(t, status.Diffs(), "options", "", "com.docker.network.bridge.enable_icc=true") }) t.Run("ipam options", func(t *testing.T) { nw := &network.Network{ Name: nwName, - State: "present", + State: network.StatePresent, IPAM: dc.IPAMOptions{ Driver: network.DefaultIPAMDriver, Config: []dc.IPAMConfig{ @@ -219,14 +219,14 @@ func TestNetworkCheck(t *testing.T) { require.NoError(t, err) assert.True(t, status.HasChanges()) assert.True(t, len(status.Diffs()) > 1) - comparison.AssertDiff(t, status.Diffs(), nwName, "present", "present") + comparison.AssertDiff(t, status.Diffs(), nwName, string(network.StatePresent), string(network.StatePresent)) comparison.AssertDiff(t, status.Diffs(), "ipam_config", "", "subnet: 192.168.129.0/24") }) t.Run("internal", func(t *testing.T) { nw := &network.Network{ Name: nwName, - State: "present", + State: network.StatePresent, Internal: true, Force: true, } @@ -242,14 +242,14 @@ func TestNetworkCheck(t *testing.T) { require.NoError(t, err) assert.True(t, status.HasChanges()) assert.True(t, len(status.Diffs()) > 1) - comparison.AssertDiff(t, status.Diffs(), nwName, "present", "present") + comparison.AssertDiff(t, status.Diffs(), nwName, string(network.StatePresent), string(network.StatePresent)) comparison.AssertDiff(t, status.Diffs(), "internal", "false", "true") }) t.Run("ipv6", func(t *testing.T) { nw := &network.Network{ Name: nwName, - State: "present", + State: network.StatePresent, IPv6: true, Force: true, } @@ -265,14 +265,14 @@ func TestNetworkCheck(t *testing.T) { require.NoError(t, err) assert.True(t, status.HasChanges()) assert.True(t, len(status.Diffs()) > 1) - comparison.AssertDiff(t, status.Diffs(), nwName, "present", "present") + comparison.AssertDiff(t, status.Diffs(), nwName, string(network.StatePresent), string(network.StatePresent)) comparison.AssertDiff(t, status.Diffs(), "ipv6", "false", "true") }) }) }) t.Run("docker api error", func(t *testing.T) { - nw := &network.Network{Name: nwName, State: "present"} + nw := &network.Network{Name: nwName, State: network.StatePresent} c := &mockClient{} nw.SetClient(c) c.On("FindNetwork", nwName).Return(nil, errors.New("error")) @@ -303,7 +303,7 @@ func TestNetworkApply(t *testing.T) { t.Run("state: present", func(t *testing.T) { t.Run("network does not exist", func(t *testing.T) { - nw := &network.Network{Name: nwName, State: "present"} + nw := &network.Network{Name: nwName, State: network.StatePresent} c := &mockClient{} nw.SetClient(c) c.On("FindNetwork", nwName).Return(nil, nil) @@ -318,7 +318,7 @@ func TestNetworkApply(t *testing.T) { }) t.Run("network exists, force: false", func(t *testing.T) { - nw := &network.Network{Name: nwName, State: "present"} + nw := &network.Network{Name: nwName, State: network.StatePresent} c := &mockClient{} nw.SetClient(c) c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName}, nil) @@ -331,7 +331,7 @@ func TestNetworkApply(t *testing.T) { }) t.Run("network exists, force: true", func(t *testing.T) { - nw := &network.Network{Name: nwName, State: "present", Force: true} + nw := &network.Network{Name: nwName, State: network.StatePresent, Force: true} c := &mockClient{} nw.SetClient(c) c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName}, nil) @@ -347,7 +347,7 @@ func TestNetworkApply(t *testing.T) { }) t.Run("docker create network error", func(t *testing.T) { - nw := &network.Network{Name: nwName, State: "present"} + nw := &network.Network{Name: nwName, State: network.StatePresent} c := &mockClient{} nw.SetClient(c) c.On("FindNetwork", nwName).Return(nil, nil) @@ -360,7 +360,7 @@ func TestNetworkApply(t *testing.T) { }) t.Run("docker remove network error", func(t *testing.T) { - nw := &network.Network{Name: nwName, State: "present", Force: true} + nw := &network.Network{Name: nwName, State: network.StatePresent, Force: true} c := &mockClient{} nw.SetClient(c) c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName, Driver: "test"}, nil) @@ -374,7 +374,7 @@ func TestNetworkApply(t *testing.T) { t.Run("state: absent", func(t *testing.T) { t.Run("network exists", func(t *testing.T) { - nw := &network.Network{Name: nwName, State: "absent"} + nw := &network.Network{Name: nwName, State: network.StateAbsent} c := &mockClient{} nw.SetClient(c) c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName}, nil) @@ -387,7 +387,7 @@ func TestNetworkApply(t *testing.T) { }) t.Run("network does not exist", func(t *testing.T) { - nw := &network.Network{Name: nwName, State: "absent"} + nw := &network.Network{Name: nwName, State: network.StateAbsent} c := &mockClient{} nw.SetClient(c) c.On("FindNetwork", nwName).Return(nil, nil) @@ -399,7 +399,7 @@ func TestNetworkApply(t *testing.T) { }) t.Run("docker remove network error", func(t *testing.T) { - nw := &network.Network{Name: nwName, State: "absent"} + nw := &network.Network{Name: nwName, State: network.StateAbsent} c := &mockClient{} nw.SetClient(c) c.On("FindNetwork", nwName).Return(&dc.Network{Name: nwName}, nil)