67ebcd6dcf added an exception for
the "host-gateway" magic value to the validation rules, but didn't
add thise value to any of the tests.
This patch adds the magic value to tests, to verify the validation
is skipped for this magic value.
Note that validation on the client side is "optional" and mostly
done to provide a more user-friendly error message for regular
values (IP-addresses).
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
1255 lines
40 KiB
Go
1255 lines
40 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/api/types/container"
|
|
mounttypes "github.com/docker/docker/api/types/mount"
|
|
"github.com/docker/docker/api/types/swarm"
|
|
"gotest.tools/v3/assert"
|
|
is "gotest.tools/v3/assert/cmp"
|
|
)
|
|
|
|
func TestUpdateServiceArgs(t *testing.T) {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("args", "the \"new args\"")
|
|
|
|
spec := &swarm.ServiceSpec{
|
|
TaskTemplate: swarm.TaskSpec{
|
|
ContainerSpec: &swarm.ContainerSpec{},
|
|
},
|
|
}
|
|
cspec := spec.TaskTemplate.ContainerSpec
|
|
cspec.Args = []string{"old", "args"}
|
|
|
|
updateService(context.TODO(), nil, flags, spec)
|
|
assert.Check(t, is.DeepEqual([]string{"the", "new args"}, cspec.Args))
|
|
}
|
|
|
|
func TestUpdateLabels(t *testing.T) {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("label-add", "toadd=newlabel")
|
|
flags.Set("label-rm", "toremove")
|
|
|
|
labels := map[string]string{
|
|
"toremove": "thelabeltoremove",
|
|
"tokeep": "value",
|
|
}
|
|
|
|
updateLabels(flags, &labels)
|
|
assert.Check(t, is.Len(labels, 2))
|
|
assert.Check(t, is.Equal("value", labels["tokeep"]))
|
|
assert.Check(t, is.Equal("newlabel", labels["toadd"]))
|
|
}
|
|
|
|
func TestUpdateLabelsRemoveALabelThatDoesNotExist(t *testing.T) {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("label-rm", "dne")
|
|
|
|
labels := map[string]string{"foo": "theoldlabel"}
|
|
updateLabels(flags, &labels)
|
|
assert.Check(t, is.Len(labels, 1))
|
|
}
|
|
|
|
func TestUpdatePlacementConstraints(t *testing.T) {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("constraint-add", "node=toadd")
|
|
flags.Set("constraint-rm", "node!=toremove")
|
|
|
|
placement := &swarm.Placement{
|
|
Constraints: []string{"node!=toremove", "container=tokeep"},
|
|
}
|
|
|
|
updatePlacementConstraints(flags, placement)
|
|
assert.Assert(t, is.Len(placement.Constraints, 2))
|
|
assert.Check(t, is.Equal("container=tokeep", placement.Constraints[0]))
|
|
assert.Check(t, is.Equal("node=toadd", placement.Constraints[1]))
|
|
}
|
|
|
|
func TestUpdatePlacementPrefs(t *testing.T) {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("placement-pref-add", "spread=node.labels.dc")
|
|
flags.Set("placement-pref-rm", "spread=node.labels.rack")
|
|
|
|
placement := &swarm.Placement{
|
|
Preferences: []swarm.PlacementPreference{
|
|
{
|
|
Spread: &swarm.SpreadOver{
|
|
SpreadDescriptor: "node.labels.rack",
|
|
},
|
|
},
|
|
{
|
|
Spread: &swarm.SpreadOver{
|
|
SpreadDescriptor: "node.labels.row",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
updatePlacementPreferences(flags, placement)
|
|
assert.Assert(t, is.Len(placement.Preferences, 2))
|
|
assert.Check(t, is.Equal("node.labels.row", placement.Preferences[0].Spread.SpreadDescriptor))
|
|
assert.Check(t, is.Equal("node.labels.dc", placement.Preferences[1].Spread.SpreadDescriptor))
|
|
}
|
|
|
|
func TestUpdateEnvironment(t *testing.T) {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("env-add", "toadd=newenv")
|
|
flags.Set("env-rm", "toremove")
|
|
|
|
envs := []string{"toremove=theenvtoremove", "tokeep=value"}
|
|
|
|
updateEnvironment(flags, &envs)
|
|
assert.Assert(t, is.Len(envs, 2))
|
|
// Order has been removed in updateEnvironment (map)
|
|
sort.Strings(envs)
|
|
assert.Check(t, is.Equal("toadd=newenv", envs[0]))
|
|
assert.Check(t, is.Equal("tokeep=value", envs[1]))
|
|
}
|
|
|
|
func TestUpdateEnvironmentWithDuplicateValues(t *testing.T) {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("env-add", "foo=newenv")
|
|
flags.Set("env-add", "foo=dupe")
|
|
flags.Set("env-rm", "foo")
|
|
|
|
envs := []string{"foo=value"}
|
|
|
|
updateEnvironment(flags, &envs)
|
|
assert.Check(t, is.Len(envs, 0))
|
|
}
|
|
|
|
func TestUpdateEnvironmentWithDuplicateKeys(t *testing.T) {
|
|
// Test case for #25404
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("env-add", "A=b")
|
|
|
|
envs := []string{"A=c"}
|
|
|
|
updateEnvironment(flags, &envs)
|
|
assert.Assert(t, is.Len(envs, 1))
|
|
assert.Check(t, is.Equal("A=b", envs[0]))
|
|
}
|
|
|
|
func TestUpdateGroups(t *testing.T) {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("group-add", "wheel")
|
|
flags.Set("group-add", "docker")
|
|
flags.Set("group-rm", "root")
|
|
flags.Set("group-add", "foo")
|
|
flags.Set("group-rm", "docker")
|
|
|
|
groups := []string{"bar", "root"}
|
|
|
|
updateGroups(flags, &groups)
|
|
assert.Assert(t, is.Len(groups, 3))
|
|
assert.Check(t, is.Equal("bar", groups[0]))
|
|
assert.Check(t, is.Equal("foo", groups[1]))
|
|
assert.Check(t, is.Equal("wheel", groups[2]))
|
|
}
|
|
|
|
func TestUpdateDNSConfig(t *testing.T) {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
|
|
// IPv4, with duplicates
|
|
flags.Set("dns-add", "1.1.1.1")
|
|
flags.Set("dns-add", "1.1.1.1")
|
|
flags.Set("dns-add", "2.2.2.2")
|
|
flags.Set("dns-rm", "3.3.3.3")
|
|
flags.Set("dns-rm", "2.2.2.2")
|
|
// IPv6
|
|
flags.Set("dns-add", "2001:db8:abc8::1")
|
|
// Invalid dns record
|
|
assert.ErrorContains(t, flags.Set("dns-add", "x.y.z.w"), "x.y.z.w is not an ip address")
|
|
|
|
// domains with duplicates
|
|
flags.Set("dns-search-add", "example.com")
|
|
flags.Set("dns-search-add", "example.com")
|
|
flags.Set("dns-search-add", "example.org")
|
|
flags.Set("dns-search-rm", "example.org")
|
|
// Invalid dns search domain
|
|
assert.ErrorContains(t, flags.Set("dns-search-add", "example$com"), "example$com is not a valid domain")
|
|
|
|
flags.Set("dns-option-add", "ndots:9")
|
|
flags.Set("dns-option-rm", "timeout:3")
|
|
|
|
config := &swarm.DNSConfig{
|
|
Nameservers: []string{"3.3.3.3", "5.5.5.5"},
|
|
Search: []string{"localdomain"},
|
|
Options: []string{"timeout:3"},
|
|
}
|
|
|
|
updateDNSConfig(flags, &config)
|
|
|
|
assert.Assert(t, is.Len(config.Nameservers, 3))
|
|
assert.Check(t, is.Equal("1.1.1.1", config.Nameservers[0]))
|
|
assert.Check(t, is.Equal("2001:db8:abc8::1", config.Nameservers[1]))
|
|
assert.Check(t, is.Equal("5.5.5.5", config.Nameservers[2]))
|
|
|
|
assert.Assert(t, is.Len(config.Search, 2))
|
|
assert.Check(t, is.Equal("example.com", config.Search[0]))
|
|
assert.Check(t, is.Equal("localdomain", config.Search[1]))
|
|
|
|
assert.Assert(t, is.Len(config.Options, 1))
|
|
assert.Check(t, is.Equal(config.Options[0], "ndots:9"))
|
|
}
|
|
|
|
func TestUpdateMounts(t *testing.T) {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("mount-add", "type=volume,source=vol2,target=/toadd")
|
|
flags.Set("mount-rm", "/toremove")
|
|
|
|
mounts := []mounttypes.Mount{
|
|
{Target: "/toremove", Source: "vol1", Type: mounttypes.TypeBind},
|
|
{Target: "/tokeep", Source: "vol3", Type: mounttypes.TypeBind},
|
|
}
|
|
|
|
updateMounts(flags, &mounts)
|
|
assert.Assert(t, is.Len(mounts, 2))
|
|
assert.Check(t, is.Equal("/toadd", mounts[0].Target))
|
|
assert.Check(t, is.Equal("/tokeep", mounts[1].Target))
|
|
}
|
|
|
|
func TestUpdateMountsWithDuplicateMounts(t *testing.T) {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("mount-add", "type=volume,source=vol4,target=/toadd")
|
|
|
|
mounts := []mounttypes.Mount{
|
|
{Target: "/tokeep1", Source: "vol1", Type: mounttypes.TypeBind},
|
|
{Target: "/toadd", Source: "vol2", Type: mounttypes.TypeBind},
|
|
{Target: "/tokeep2", Source: "vol3", Type: mounttypes.TypeBind},
|
|
}
|
|
|
|
updateMounts(flags, &mounts)
|
|
assert.Assert(t, is.Len(mounts, 3))
|
|
assert.Check(t, is.Equal("/tokeep1", mounts[0].Target))
|
|
assert.Check(t, is.Equal("/tokeep2", mounts[1].Target))
|
|
assert.Check(t, is.Equal("/toadd", mounts[2].Target))
|
|
}
|
|
|
|
func TestUpdatePorts(t *testing.T) {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("publish-add", "1000:1000")
|
|
flags.Set("publish-rm", "333/udp")
|
|
|
|
portConfigs := []swarm.PortConfig{
|
|
{TargetPort: 333, Protocol: swarm.PortConfigProtocolUDP},
|
|
{TargetPort: 555},
|
|
}
|
|
|
|
err := updatePorts(flags, &portConfigs)
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, is.Len(portConfigs, 2))
|
|
// Do a sort to have the order (might have changed by map)
|
|
targetPorts := []int{int(portConfigs[0].TargetPort), int(portConfigs[1].TargetPort)}
|
|
sort.Ints(targetPorts)
|
|
assert.Check(t, is.Equal(555, targetPorts[0]))
|
|
assert.Check(t, is.Equal(1000, targetPorts[1]))
|
|
}
|
|
|
|
func TestUpdatePortsDuplicate(t *testing.T) {
|
|
// Test case for #25375
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("publish-add", "80:80")
|
|
|
|
portConfigs := []swarm.PortConfig{
|
|
{
|
|
TargetPort: 80,
|
|
PublishedPort: 80,
|
|
Protocol: swarm.PortConfigProtocolTCP,
|
|
PublishMode: swarm.PortConfigPublishModeIngress,
|
|
},
|
|
}
|
|
|
|
err := updatePorts(flags, &portConfigs)
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, is.Len(portConfigs, 1))
|
|
assert.Check(t, is.Equal(uint32(80), portConfigs[0].TargetPort))
|
|
}
|
|
|
|
func TestUpdateHealthcheckTable(t *testing.T) {
|
|
type test struct {
|
|
flags [][2]string
|
|
initial *container.HealthConfig
|
|
expected *container.HealthConfig
|
|
err string
|
|
}
|
|
testCases := []test{
|
|
{
|
|
flags: [][2]string{{"no-healthcheck", "true"}},
|
|
initial: &container.HealthConfig{Test: []string{"CMD-SHELL", "cmd1"}, Retries: 10},
|
|
expected: &container.HealthConfig{Test: []string{"NONE"}},
|
|
},
|
|
{
|
|
flags: [][2]string{{"health-cmd", "cmd1"}},
|
|
initial: &container.HealthConfig{Test: []string{"NONE"}},
|
|
expected: &container.HealthConfig{Test: []string{"CMD-SHELL", "cmd1"}},
|
|
},
|
|
{
|
|
flags: [][2]string{{"health-retries", "10"}},
|
|
initial: &container.HealthConfig{Test: []string{"NONE"}},
|
|
expected: &container.HealthConfig{Retries: 10},
|
|
},
|
|
{
|
|
flags: [][2]string{{"health-retries", "10"}},
|
|
initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}},
|
|
expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Retries: 10},
|
|
},
|
|
{
|
|
flags: [][2]string{{"health-interval", "1m"}},
|
|
initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}},
|
|
expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Interval: time.Minute},
|
|
},
|
|
{
|
|
flags: [][2]string{{"health-cmd", ""}},
|
|
initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Retries: 10},
|
|
expected: &container.HealthConfig{Retries: 10},
|
|
},
|
|
{
|
|
flags: [][2]string{{"health-retries", "0"}},
|
|
initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Retries: 10},
|
|
expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}},
|
|
},
|
|
{
|
|
flags: [][2]string{{"health-start-period", "1m"}},
|
|
initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}},
|
|
expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, StartPeriod: time.Minute},
|
|
},
|
|
{
|
|
flags: [][2]string{{"health-cmd", "cmd1"}, {"no-healthcheck", "true"}},
|
|
err: "--no-healthcheck conflicts with --health-* options",
|
|
},
|
|
{
|
|
flags: [][2]string{{"health-interval", "10m"}, {"no-healthcheck", "true"}},
|
|
err: "--no-healthcheck conflicts with --health-* options",
|
|
},
|
|
{
|
|
flags: [][2]string{{"health-timeout", "1m"}, {"no-healthcheck", "true"}},
|
|
err: "--no-healthcheck conflicts with --health-* options",
|
|
},
|
|
}
|
|
for i, c := range testCases {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
for _, flag := range c.flags {
|
|
flags.Set(flag[0], flag[1])
|
|
}
|
|
cspec := &swarm.ContainerSpec{
|
|
Healthcheck: c.initial,
|
|
}
|
|
err := updateHealthcheck(flags, cspec)
|
|
if c.err != "" {
|
|
assert.Error(t, err, c.err)
|
|
} else {
|
|
assert.NilError(t, err)
|
|
if !reflect.DeepEqual(cspec.Healthcheck, c.expected) {
|
|
t.Errorf("incorrect result for test %d, expected health config:\n\t%#v\ngot:\n\t%#v", i, c.expected, cspec.Healthcheck)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUpdateHosts(t *testing.T) {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("host-add", "example.net:2.2.2.2")
|
|
flags.Set("host-add", "ipv6.net:2001:db8:abc8::1")
|
|
// adding the special "host-gateway" target should work
|
|
flags.Set("host-add", "host.docker.internal:host-gateway")
|
|
// remove with ipv6 should work
|
|
flags.Set("host-rm", "example.net:2001:db8:abc8::1")
|
|
// just hostname should work as well
|
|
flags.Set("host-rm", "example.net")
|
|
// removing the special "host-gateway" target should work
|
|
flags.Set("host-rm", "gateway.docker.internal:host-gateway")
|
|
// bad format error
|
|
assert.ErrorContains(t, flags.Set("host-add", "$example.com$"), `bad format for add-host: "$example.com$"`)
|
|
|
|
hosts := []string{"1.2.3.4 example.com", "4.3.2.1 example.org", "2001:db8:abc8::1 example.net", "gateway.docker.internal:host-gateway"}
|
|
expected := []string{"1.2.3.4 example.com", "4.3.2.1 example.org", "2.2.2.2 example.net", "2001:db8:abc8::1 ipv6.net", "host-gateway host.docker.internal"}
|
|
|
|
err := updateHosts(flags, &hosts)
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.DeepEqual(expected, hosts))
|
|
}
|
|
|
|
func TestUpdateHostsPreservesOrder(t *testing.T) {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("host-add", "foobar:127.0.0.2")
|
|
flags.Set("host-add", "foobar:127.0.0.1")
|
|
flags.Set("host-add", "foobar:127.0.0.3")
|
|
|
|
hosts := []string{}
|
|
err := updateHosts(flags, &hosts)
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.DeepEqual([]string{"127.0.0.2 foobar", "127.0.0.1 foobar", "127.0.0.3 foobar"}, hosts))
|
|
}
|
|
|
|
func TestUpdateHostsReplaceEntry(t *testing.T) {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("host-add", "foobar:127.0.0.4")
|
|
flags.Set("host-rm", "foobar:127.0.0.2")
|
|
|
|
hosts := []string{"127.0.0.2 foobar", "127.0.0.1 foobar", "127.0.0.3 foobar"}
|
|
|
|
err := updateHosts(flags, &hosts)
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.DeepEqual([]string{"127.0.0.1 foobar", "127.0.0.3 foobar", "127.0.0.4 foobar"}, hosts))
|
|
}
|
|
|
|
func TestUpdateHostsRemoveHost(t *testing.T) {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("host-rm", "host1")
|
|
|
|
hosts := []string{"127.0.0.2 host3 host1 host2 host4", "127.0.0.1 host1 host4", "127.0.0.3 host1"}
|
|
|
|
err := updateHosts(flags, &hosts)
|
|
assert.NilError(t, err)
|
|
|
|
// Removing host `host1` should remove the entry from each line it appears in.
|
|
// If there are no other hosts in the entry, the entry itself should be removed.
|
|
assert.Check(t, is.DeepEqual([]string{"127.0.0.2 host3 host2 host4", "127.0.0.1 host4"}, hosts))
|
|
}
|
|
|
|
func TestUpdateHostsRemoveHostIP(t *testing.T) {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("host-rm", "host1:127.0.0.1")
|
|
|
|
hosts := []string{"127.0.0.2 host3 host1 host2 host4", "127.0.0.1 host1 host4", "127.0.0.3 host1", "127.0.0.1 host1"}
|
|
|
|
err := updateHosts(flags, &hosts)
|
|
assert.NilError(t, err)
|
|
|
|
// Removing host `host1` should remove the entry from each line it appears in,
|
|
// but only if the IP-address matches. If there are no other hosts in the entry,
|
|
// the entry itself should be removed.
|
|
assert.Check(t, is.DeepEqual([]string{"127.0.0.2 host3 host1 host2 host4", "127.0.0.1 host4", "127.0.0.3 host1"}, hosts))
|
|
}
|
|
|
|
func TestUpdateHostsRemoveAll(t *testing.T) {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("host-add", "host-three:127.0.0.4")
|
|
flags.Set("host-add", "host-one:127.0.0.5")
|
|
flags.Set("host-rm", "host-one")
|
|
|
|
hosts := []string{"127.0.0.1 host-one", "127.0.0.2 host-two", "127.0.0.3 host-one"}
|
|
|
|
err := updateHosts(flags, &hosts)
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.DeepEqual([]string{"127.0.0.2 host-two", "127.0.0.4 host-three", "127.0.0.5 host-one"}, hosts))
|
|
}
|
|
|
|
func TestUpdatePortsRmWithProtocol(t *testing.T) {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("publish-add", "8081:81")
|
|
flags.Set("publish-add", "8082:82")
|
|
flags.Set("publish-rm", "80")
|
|
flags.Set("publish-rm", "81/tcp")
|
|
flags.Set("publish-rm", "82/udp")
|
|
|
|
portConfigs := []swarm.PortConfig{
|
|
{
|
|
TargetPort: 80,
|
|
PublishedPort: 8080,
|
|
Protocol: swarm.PortConfigProtocolTCP,
|
|
PublishMode: swarm.PortConfigPublishModeIngress,
|
|
},
|
|
}
|
|
|
|
err := updatePorts(flags, &portConfigs)
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, is.Len(portConfigs, 2))
|
|
assert.Check(t, is.Equal(uint32(81), portConfigs[0].TargetPort))
|
|
assert.Check(t, is.Equal(uint32(82), portConfigs[1].TargetPort))
|
|
}
|
|
|
|
type secretAPIClientMock struct {
|
|
listResult []swarm.Secret
|
|
}
|
|
|
|
func (s secretAPIClientMock) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) {
|
|
return s.listResult, nil
|
|
}
|
|
func (s secretAPIClientMock) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error) {
|
|
return types.SecretCreateResponse{}, nil
|
|
}
|
|
func (s secretAPIClientMock) SecretRemove(ctx context.Context, id string) error {
|
|
return nil
|
|
}
|
|
func (s secretAPIClientMock) SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error) {
|
|
return swarm.Secret{}, []byte{}, nil
|
|
}
|
|
func (s secretAPIClientMock) SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error {
|
|
return nil
|
|
}
|
|
|
|
// TestUpdateSecretUpdateInPlace tests the ability to update the "target" of an secret with "docker service update"
|
|
// by combining "--secret-rm" and "--secret-add" for the same secret.
|
|
func TestUpdateSecretUpdateInPlace(t *testing.T) {
|
|
apiClient := secretAPIClientMock{
|
|
listResult: []swarm.Secret{
|
|
{
|
|
ID: "tn9qiblgnuuut11eufquw5dev",
|
|
Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "foo"}},
|
|
},
|
|
},
|
|
}
|
|
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("secret-add", "source=foo,target=foo2")
|
|
flags.Set("secret-rm", "foo")
|
|
|
|
secrets := []*swarm.SecretReference{
|
|
{
|
|
File: &swarm.SecretReferenceFileTarget{
|
|
Name: "foo",
|
|
UID: "0",
|
|
GID: "0",
|
|
Mode: 292,
|
|
},
|
|
SecretID: "tn9qiblgnuuut11eufquw5dev",
|
|
SecretName: "foo",
|
|
},
|
|
}
|
|
|
|
updatedSecrets, err := getUpdatedSecrets(apiClient, flags, secrets)
|
|
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, is.Len(updatedSecrets, 1))
|
|
assert.Check(t, is.Equal("tn9qiblgnuuut11eufquw5dev", updatedSecrets[0].SecretID))
|
|
assert.Check(t, is.Equal("foo", updatedSecrets[0].SecretName))
|
|
assert.Check(t, is.Equal("foo2", updatedSecrets[0].File.Name))
|
|
}
|
|
|
|
func TestUpdateReadOnly(t *testing.T) {
|
|
spec := &swarm.ServiceSpec{
|
|
TaskTemplate: swarm.TaskSpec{
|
|
ContainerSpec: &swarm.ContainerSpec{},
|
|
},
|
|
}
|
|
cspec := spec.TaskTemplate.ContainerSpec
|
|
|
|
// Update with --read-only=true, changed to true
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("read-only", "true")
|
|
updateService(context.TODO(), nil, flags, spec)
|
|
assert.Check(t, cspec.ReadOnly)
|
|
|
|
// Update without --read-only, no change
|
|
flags = newUpdateCommand(nil).Flags()
|
|
updateService(context.TODO(), nil, flags, spec)
|
|
assert.Check(t, cspec.ReadOnly)
|
|
|
|
// Update with --read-only=false, changed to false
|
|
flags = newUpdateCommand(nil).Flags()
|
|
flags.Set("read-only", "false")
|
|
updateService(context.TODO(), nil, flags, spec)
|
|
assert.Check(t, !cspec.ReadOnly)
|
|
}
|
|
|
|
func TestUpdateInit(t *testing.T) {
|
|
spec := &swarm.ServiceSpec{
|
|
TaskTemplate: swarm.TaskSpec{
|
|
ContainerSpec: &swarm.ContainerSpec{},
|
|
},
|
|
}
|
|
cspec := spec.TaskTemplate.ContainerSpec
|
|
|
|
// Update with --init=true
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("init", "true")
|
|
updateService(context.TODO(), nil, flags, spec)
|
|
assert.Check(t, is.Equal(true, *cspec.Init))
|
|
|
|
// Update without --init, no change
|
|
flags = newUpdateCommand(nil).Flags()
|
|
updateService(context.TODO(), nil, flags, spec)
|
|
assert.Check(t, is.Equal(true, *cspec.Init))
|
|
|
|
// Update with --init=false
|
|
flags = newUpdateCommand(nil).Flags()
|
|
flags.Set("init", "false")
|
|
updateService(context.TODO(), nil, flags, spec)
|
|
assert.Check(t, is.Equal(false, *cspec.Init))
|
|
}
|
|
|
|
func TestUpdateStopSignal(t *testing.T) {
|
|
spec := &swarm.ServiceSpec{
|
|
TaskTemplate: swarm.TaskSpec{
|
|
ContainerSpec: &swarm.ContainerSpec{},
|
|
},
|
|
}
|
|
cspec := spec.TaskTemplate.ContainerSpec
|
|
|
|
// Update with --stop-signal=SIGUSR1
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set("stop-signal", "SIGUSR1")
|
|
updateService(context.TODO(), nil, flags, spec)
|
|
assert.Check(t, is.Equal("SIGUSR1", cspec.StopSignal))
|
|
|
|
// Update without --stop-signal, no change
|
|
flags = newUpdateCommand(nil).Flags()
|
|
updateService(context.TODO(), nil, flags, spec)
|
|
assert.Check(t, is.Equal("SIGUSR1", cspec.StopSignal))
|
|
|
|
// Update with --stop-signal=SIGWINCH
|
|
flags = newUpdateCommand(nil).Flags()
|
|
flags.Set("stop-signal", "SIGWINCH")
|
|
updateService(context.TODO(), nil, flags, spec)
|
|
assert.Check(t, is.Equal("SIGWINCH", cspec.StopSignal))
|
|
}
|
|
|
|
func TestUpdateIsolationValid(t *testing.T) {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
err := flags.Set("isolation", "process")
|
|
assert.NilError(t, err)
|
|
spec := swarm.ServiceSpec{
|
|
TaskTemplate: swarm.TaskSpec{
|
|
ContainerSpec: &swarm.ContainerSpec{},
|
|
},
|
|
}
|
|
err = updateService(context.Background(), nil, flags, &spec)
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.Equal(container.IsolationProcess, spec.TaskTemplate.ContainerSpec.Isolation))
|
|
}
|
|
|
|
// TestUpdateLimitsReservations tests that limits and reservations are updated,
|
|
// and that values are not updated are not reset to their default value
|
|
func TestUpdateLimitsReservations(t *testing.T) {
|
|
spec := swarm.ServiceSpec{
|
|
TaskTemplate: swarm.TaskSpec{
|
|
ContainerSpec: &swarm.ContainerSpec{},
|
|
},
|
|
}
|
|
|
|
// test that updating works if the service did not previously
|
|
// have limits set (https://github.com/moby/moby/issues/38363)
|
|
flags := newUpdateCommand(nil).Flags()
|
|
err := flags.Set(flagLimitCPU, "2")
|
|
assert.NilError(t, err)
|
|
err = flags.Set(flagLimitMemory, "200M")
|
|
assert.NilError(t, err)
|
|
err = updateService(context.Background(), nil, flags, &spec)
|
|
assert.NilError(t, err)
|
|
|
|
spec = swarm.ServiceSpec{
|
|
TaskTemplate: swarm.TaskSpec{
|
|
ContainerSpec: &swarm.ContainerSpec{},
|
|
},
|
|
}
|
|
|
|
// test that updating works if the service did not previously
|
|
// have reservations set (https://github.com/moby/moby/issues/38363)
|
|
flags = newUpdateCommand(nil).Flags()
|
|
err = flags.Set(flagReserveCPU, "2")
|
|
assert.NilError(t, err)
|
|
err = flags.Set(flagReserveMemory, "200M")
|
|
assert.NilError(t, err)
|
|
err = updateService(context.Background(), nil, flags, &spec)
|
|
assert.NilError(t, err)
|
|
|
|
spec = swarm.ServiceSpec{
|
|
TaskTemplate: swarm.TaskSpec{
|
|
ContainerSpec: &swarm.ContainerSpec{},
|
|
Resources: &swarm.ResourceRequirements{
|
|
Limits: &swarm.Resources{
|
|
NanoCPUs: 1000000000,
|
|
MemoryBytes: 104857600,
|
|
},
|
|
Reservations: &swarm.Resources{
|
|
NanoCPUs: 1000000000,
|
|
MemoryBytes: 104857600,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
flags = newUpdateCommand(nil).Flags()
|
|
err = flags.Set(flagLimitCPU, "2")
|
|
assert.NilError(t, err)
|
|
err = flags.Set(flagReserveCPU, "2")
|
|
assert.NilError(t, err)
|
|
err = updateService(context.Background(), nil, flags, &spec)
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.NanoCPUs, int64(2000000000)))
|
|
assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.MemoryBytes, int64(104857600)))
|
|
assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.NanoCPUs, int64(2000000000)))
|
|
assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.MemoryBytes, int64(104857600)))
|
|
|
|
flags = newUpdateCommand(nil).Flags()
|
|
err = flags.Set(flagLimitMemory, "200M")
|
|
assert.NilError(t, err)
|
|
err = flags.Set(flagReserveMemory, "200M")
|
|
assert.NilError(t, err)
|
|
err = updateService(context.Background(), nil, flags, &spec)
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.NanoCPUs, int64(2000000000)))
|
|
assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Limits.MemoryBytes, int64(209715200)))
|
|
assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.NanoCPUs, int64(2000000000)))
|
|
assert.Check(t, is.Equal(spec.TaskTemplate.Resources.Reservations.MemoryBytes, int64(209715200)))
|
|
}
|
|
|
|
func TestUpdateIsolationInvalid(t *testing.T) {
|
|
// validation depends on daemon os / version so validation should be done on the daemon side
|
|
flags := newUpdateCommand(nil).Flags()
|
|
err := flags.Set("isolation", "test")
|
|
assert.NilError(t, err)
|
|
spec := swarm.ServiceSpec{
|
|
TaskTemplate: swarm.TaskSpec{
|
|
ContainerSpec: &swarm.ContainerSpec{},
|
|
},
|
|
}
|
|
err = updateService(context.Background(), nil, flags, &spec)
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.Equal(container.Isolation("test"), spec.TaskTemplate.ContainerSpec.Isolation))
|
|
}
|
|
|
|
func TestAddGenericResources(t *testing.T) {
|
|
task := &swarm.TaskSpec{}
|
|
flags := newUpdateCommand(nil).Flags()
|
|
|
|
assert.Check(t, addGenericResources(flags, task))
|
|
|
|
flags.Set(flagGenericResourcesAdd, "foo=1")
|
|
assert.Check(t, addGenericResources(flags, task))
|
|
assert.Check(t, is.Len(task.Resources.Reservations.GenericResources, 1))
|
|
|
|
// Checks that foo isn't added a 2nd time
|
|
flags = newUpdateCommand(nil).Flags()
|
|
flags.Set(flagGenericResourcesAdd, "bar=1")
|
|
assert.Check(t, addGenericResources(flags, task))
|
|
assert.Check(t, is.Len(task.Resources.Reservations.GenericResources, 2))
|
|
}
|
|
|
|
func TestRemoveGenericResources(t *testing.T) {
|
|
task := &swarm.TaskSpec{}
|
|
flags := newUpdateCommand(nil).Flags()
|
|
|
|
assert.Check(t, removeGenericResources(flags, task))
|
|
|
|
flags.Set(flagGenericResourcesRemove, "foo")
|
|
assert.Check(t, is.ErrorContains(removeGenericResources(flags, task), ""))
|
|
|
|
flags = newUpdateCommand(nil).Flags()
|
|
flags.Set(flagGenericResourcesAdd, "foo=1")
|
|
addGenericResources(flags, task)
|
|
flags = newUpdateCommand(nil).Flags()
|
|
flags.Set(flagGenericResourcesAdd, "bar=1")
|
|
addGenericResources(flags, task)
|
|
|
|
flags = newUpdateCommand(nil).Flags()
|
|
flags.Set(flagGenericResourcesRemove, "foo")
|
|
assert.Check(t, removeGenericResources(flags, task))
|
|
assert.Check(t, is.Len(task.Resources.Reservations.GenericResources, 1))
|
|
}
|
|
|
|
func TestUpdateNetworks(t *testing.T) {
|
|
ctx := context.Background()
|
|
nws := []types.NetworkResource{
|
|
{Name: "aaa-network", ID: "id555"},
|
|
{Name: "mmm-network", ID: "id999"},
|
|
{Name: "zzz-network", ID: "id111"},
|
|
}
|
|
|
|
client := &fakeClient{
|
|
networkInspectFunc: func(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error) {
|
|
for _, network := range nws {
|
|
if network.ID == networkID || network.Name == networkID {
|
|
return network, nil
|
|
}
|
|
}
|
|
return types.NetworkResource{}, fmt.Errorf("network not found: %s", networkID)
|
|
},
|
|
}
|
|
|
|
svc := swarm.ServiceSpec{
|
|
TaskTemplate: swarm.TaskSpec{
|
|
ContainerSpec: &swarm.ContainerSpec{},
|
|
Networks: []swarm.NetworkAttachmentConfig{
|
|
{Target: "id999"},
|
|
},
|
|
},
|
|
}
|
|
|
|
flags := newUpdateCommand(nil).Flags()
|
|
err := flags.Set(flagNetworkAdd, "aaa-network")
|
|
assert.NilError(t, err)
|
|
err = updateService(ctx, client, flags, &svc)
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks))
|
|
|
|
flags = newUpdateCommand(nil).Flags()
|
|
err = flags.Set(flagNetworkAdd, "aaa-network")
|
|
assert.NilError(t, err)
|
|
err = updateService(ctx, client, flags, &svc)
|
|
assert.Error(t, err, "service is already attached to network aaa-network")
|
|
assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks))
|
|
|
|
flags = newUpdateCommand(nil).Flags()
|
|
err = flags.Set(flagNetworkAdd, "id555")
|
|
assert.NilError(t, err)
|
|
err = updateService(ctx, client, flags, &svc)
|
|
assert.Error(t, err, "service is already attached to network id555")
|
|
assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks))
|
|
|
|
flags = newUpdateCommand(nil).Flags()
|
|
err = flags.Set(flagNetworkRemove, "id999")
|
|
assert.NilError(t, err)
|
|
err = updateService(ctx, client, flags, &svc)
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id555"}}, svc.TaskTemplate.Networks))
|
|
|
|
flags = newUpdateCommand(nil).Flags()
|
|
err = flags.Set(flagNetworkAdd, "mmm-network")
|
|
assert.NilError(t, err)
|
|
err = flags.Set(flagNetworkRemove, "aaa-network")
|
|
assert.NilError(t, err)
|
|
err = updateService(ctx, client, flags, &svc)
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id999"}}, svc.TaskTemplate.Networks))
|
|
}
|
|
|
|
func TestUpdateMaxReplicas(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
svc := swarm.ServiceSpec{
|
|
TaskTemplate: swarm.TaskSpec{
|
|
ContainerSpec: &swarm.ContainerSpec{},
|
|
Placement: &swarm.Placement{
|
|
MaxReplicas: 1,
|
|
},
|
|
},
|
|
}
|
|
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set(flagMaxReplicas, "2")
|
|
err := updateService(ctx, nil, flags, &svc)
|
|
assert.NilError(t, err)
|
|
|
|
assert.DeepEqual(t, svc.TaskTemplate.Placement, &swarm.Placement{MaxReplicas: uint64(2)})
|
|
}
|
|
|
|
func TestUpdateSysCtls(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
tests := []struct {
|
|
name string
|
|
spec map[string]string
|
|
add []string
|
|
rm []string
|
|
expected map[string]string
|
|
}{
|
|
{
|
|
name: "from scratch",
|
|
add: []string{"sysctl.zet=value-99", "sysctl.alpha=value-1"},
|
|
expected: map[string]string{"sysctl.zet": "value-99", "sysctl.alpha": "value-1"},
|
|
},
|
|
{
|
|
name: "append new",
|
|
spec: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
|
|
add: []string{"new.sysctl=newvalue"},
|
|
expected: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2", "new.sysctl": "newvalue"},
|
|
},
|
|
{
|
|
name: "append duplicate is a no-op",
|
|
spec: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
|
|
add: []string{"sysctl.one=value-1"},
|
|
expected: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
|
|
},
|
|
{
|
|
name: "remove and append existing is a no-op",
|
|
spec: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
|
|
add: []string{"sysctl.one=value-1"},
|
|
rm: []string{"sysctl.one=value-1"},
|
|
expected: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
|
|
},
|
|
{
|
|
name: "remove and append new should append",
|
|
spec: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
|
|
add: []string{"new.sysctl=newvalue"},
|
|
rm: []string{"new.sysctl=newvalue"},
|
|
expected: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2", "new.sysctl": "newvalue"},
|
|
},
|
|
{
|
|
name: "update existing",
|
|
spec: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
|
|
add: []string{"sysctl.one=newvalue"},
|
|
expected: map[string]string{"sysctl.one": "newvalue", "sysctl.two": "value-2"},
|
|
},
|
|
{
|
|
name: "update existing twice",
|
|
spec: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
|
|
add: []string{"sysctl.one=newvalue", "sysctl.one=evennewervalue"},
|
|
expected: map[string]string{"sysctl.one": "evennewervalue", "sysctl.two": "value-2"},
|
|
},
|
|
{
|
|
name: "remove all",
|
|
spec: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
|
|
rm: []string{"sysctl.one=value-1", "sysctl.two=value-2"},
|
|
expected: map[string]string{},
|
|
},
|
|
{
|
|
name: "remove by key",
|
|
spec: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
|
|
rm: []string{"sysctl.one"},
|
|
expected: map[string]string{"sysctl.two": "value-2"},
|
|
},
|
|
{
|
|
name: "remove by key and different value",
|
|
spec: map[string]string{"sysctl.one": "value-1", "sysctl.two": "value-2"},
|
|
rm: []string{"sysctl.one=anyvalueyoulike"},
|
|
expected: map[string]string{"sysctl.two": "value-2"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
svc := swarm.ServiceSpec{
|
|
TaskTemplate: swarm.TaskSpec{
|
|
ContainerSpec: &swarm.ContainerSpec{Sysctls: tc.spec},
|
|
},
|
|
}
|
|
flags := newUpdateCommand(nil).Flags()
|
|
for _, v := range tc.add {
|
|
assert.NilError(t, flags.Set(flagSysCtlAdd, v))
|
|
}
|
|
for _, v := range tc.rm {
|
|
assert.NilError(t, flags.Set(flagSysCtlRemove, v))
|
|
}
|
|
err := updateService(ctx, &fakeClient{}, flags, &svc)
|
|
assert.NilError(t, err)
|
|
if !assert.Check(t, is.DeepEqual(svc.TaskTemplate.ContainerSpec.Sysctls, tc.expected)) {
|
|
t.Logf("expected: %v", tc.expected)
|
|
t.Logf("actual: %v", svc.TaskTemplate.ContainerSpec.Sysctls)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpdateGetUpdatedConfigs(t *testing.T) {
|
|
// cannedConfigs is a set of configs that we'll use over and over in the
|
|
// tests. it's a map of Name to Config
|
|
cannedConfigs := map[string]*swarm.Config{
|
|
"bar": {
|
|
ID: "barID",
|
|
Spec: swarm.ConfigSpec{
|
|
Annotations: swarm.Annotations{
|
|
Name: "bar",
|
|
},
|
|
},
|
|
},
|
|
"cred": {
|
|
ID: "credID",
|
|
Spec: swarm.ConfigSpec{
|
|
Annotations: swarm.Annotations{
|
|
Name: "cred",
|
|
},
|
|
},
|
|
},
|
|
"newCred": {
|
|
ID: "newCredID",
|
|
Spec: swarm.ConfigSpec{
|
|
Annotations: swarm.Annotations{
|
|
Name: "newCred",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
// cannedConfigRefs is the same thing, but with config references instead
|
|
// instead of ID, however, it just maps an arbitrary string value. this is
|
|
// so we could have multiple config refs using the same config
|
|
cannedConfigRefs := map[string]*swarm.ConfigReference{
|
|
"fooRef": {
|
|
ConfigID: "fooID",
|
|
ConfigName: "foo",
|
|
File: &swarm.ConfigReferenceFileTarget{
|
|
Name: "foo",
|
|
UID: "0",
|
|
GID: "0",
|
|
Mode: 0444,
|
|
},
|
|
},
|
|
"barRef": {
|
|
ConfigID: "barID",
|
|
ConfigName: "bar",
|
|
File: &swarm.ConfigReferenceFileTarget{
|
|
Name: "bar",
|
|
UID: "0",
|
|
GID: "0",
|
|
Mode: 0444,
|
|
},
|
|
},
|
|
"bazRef": {
|
|
ConfigID: "bazID",
|
|
ConfigName: "baz",
|
|
File: &swarm.ConfigReferenceFileTarget{
|
|
Name: "baz",
|
|
UID: "0",
|
|
GID: "0",
|
|
Mode: 0444,
|
|
},
|
|
},
|
|
"credRef": {
|
|
ConfigID: "credID",
|
|
ConfigName: "cred",
|
|
Runtime: &swarm.ConfigReferenceRuntimeTarget{},
|
|
},
|
|
"newCredRef": {
|
|
ConfigID: "newCredID",
|
|
ConfigName: "newCred",
|
|
Runtime: &swarm.ConfigReferenceRuntimeTarget{},
|
|
},
|
|
}
|
|
|
|
type flagVal [2]string
|
|
type test struct {
|
|
// the name of the subtest
|
|
name string
|
|
// flags are the flags we'll be setting
|
|
flags []flagVal
|
|
// oldConfigs are the configs that would already be on the service
|
|
// it is a slice of strings corresponding to the the key of
|
|
// cannedConfigRefs
|
|
oldConfigs []string
|
|
// oldCredSpec is the credentialSpec being carried over from the old
|
|
// object
|
|
oldCredSpec *swarm.CredentialSpec
|
|
// lookupConfigs are the configs we're expecting to be listed. it is a
|
|
// slice of strings corresponding to the key of cannedConfigs
|
|
lookupConfigs []string
|
|
// expected is the configs we should get as a result. it is a slice of
|
|
// strings corresponding to the key in cannedConfigRefs
|
|
expected []string
|
|
}
|
|
|
|
testCases := []test{
|
|
{
|
|
name: "no configs added or removed",
|
|
oldConfigs: []string{"fooRef"},
|
|
expected: []string{"fooRef"},
|
|
}, {
|
|
name: "add a config",
|
|
flags: []flagVal{{"config-add", "bar"}},
|
|
oldConfigs: []string{"fooRef"},
|
|
lookupConfigs: []string{"bar"},
|
|
expected: []string{"fooRef", "barRef"},
|
|
}, {
|
|
name: "remove a config",
|
|
flags: []flagVal{{"config-rm", "bar"}},
|
|
oldConfigs: []string{"fooRef", "barRef"},
|
|
expected: []string{"fooRef"},
|
|
}, {
|
|
name: "include an old credential spec",
|
|
oldConfigs: []string{"credRef"},
|
|
oldCredSpec: &swarm.CredentialSpec{Config: "credID"},
|
|
expected: []string{"credRef"},
|
|
}, {
|
|
name: "add a credential spec",
|
|
oldConfigs: []string{"fooRef"},
|
|
flags: []flagVal{{"credential-spec", "config://cred"}},
|
|
lookupConfigs: []string{"cred"},
|
|
expected: []string{"fooRef", "credRef"},
|
|
}, {
|
|
name: "change a credential spec",
|
|
oldConfigs: []string{"fooRef", "credRef"},
|
|
oldCredSpec: &swarm.CredentialSpec{Config: "credID"},
|
|
flags: []flagVal{{"credential-spec", "config://newCred"}},
|
|
lookupConfigs: []string{"newCred"},
|
|
expected: []string{"fooRef", "newCredRef"},
|
|
}, {
|
|
name: "credential spec no longer config",
|
|
oldConfigs: []string{"fooRef", "credRef"},
|
|
oldCredSpec: &swarm.CredentialSpec{Config: "credID"},
|
|
flags: []flagVal{{"credential-spec", "file://someFile"}},
|
|
lookupConfigs: []string{},
|
|
expected: []string{"fooRef"},
|
|
}, {
|
|
name: "credential spec becomes config",
|
|
oldConfigs: []string{"fooRef"},
|
|
oldCredSpec: &swarm.CredentialSpec{File: "someFile"},
|
|
flags: []flagVal{{"credential-spec", "config://cred"}},
|
|
lookupConfigs: []string{"cred"},
|
|
expected: []string{"fooRef", "credRef"},
|
|
}, {
|
|
name: "remove credential spec",
|
|
oldConfigs: []string{"fooRef", "credRef"},
|
|
oldCredSpec: &swarm.CredentialSpec{Config: "credID"},
|
|
flags: []flagVal{{"credential-spec", ""}},
|
|
lookupConfigs: []string{},
|
|
expected: []string{"fooRef"},
|
|
}, {
|
|
name: "just frick my stuff up",
|
|
// a more complicated test. add barRef, remove bazRef, keep fooRef,
|
|
// change credentialSpec from credRef to newCredRef
|
|
oldConfigs: []string{"fooRef", "bazRef", "credRef"},
|
|
oldCredSpec: &swarm.CredentialSpec{Config: "cred"},
|
|
flags: []flagVal{
|
|
{"config-add", "bar"},
|
|
{"config-rm", "baz"},
|
|
{"credential-spec", "config://newCred"},
|
|
},
|
|
lookupConfigs: []string{"bar", "newCred"},
|
|
expected: []string{"fooRef", "barRef", "newCredRef"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
for _, f := range tc.flags {
|
|
flags.Set(f[0], f[1])
|
|
}
|
|
|
|
// fakeConfigAPIClientList is actually defined in create_test.go,
|
|
// but we'll use it here as well
|
|
var fakeClient fakeConfigAPIClientList = func(_ context.Context, opts types.ConfigListOptions) ([]swarm.Config, error) {
|
|
names := opts.Filters.Get("name")
|
|
assert.Equal(t, len(names), len(tc.lookupConfigs))
|
|
|
|
configs := []swarm.Config{}
|
|
for _, lookup := range tc.lookupConfigs {
|
|
assert.Assert(t, is.Contains(names, lookup))
|
|
cfg, ok := cannedConfigs[lookup]
|
|
assert.Assert(t, ok)
|
|
configs = append(configs, *cfg)
|
|
}
|
|
return configs, nil
|
|
}
|
|
|
|
// build the actual set of old configs and the container spec
|
|
oldConfigs := []*swarm.ConfigReference{}
|
|
for _, config := range tc.oldConfigs {
|
|
cfg, ok := cannedConfigRefs[config]
|
|
assert.Assert(t, ok)
|
|
oldConfigs = append(oldConfigs, cfg)
|
|
}
|
|
|
|
containerSpec := &swarm.ContainerSpec{
|
|
Configs: oldConfigs,
|
|
Privileges: &swarm.Privileges{
|
|
CredentialSpec: tc.oldCredSpec,
|
|
},
|
|
}
|
|
|
|
finalConfigs, err := getUpdatedConfigs(fakeClient, flags, containerSpec)
|
|
assert.NilError(t, err)
|
|
|
|
// ensure that the finalConfigs consists of all of the expected
|
|
// configs
|
|
assert.Equal(t, len(finalConfigs), len(tc.expected),
|
|
"%v final configs, %v expected",
|
|
len(finalConfigs), len(tc.expected),
|
|
)
|
|
for _, expected := range tc.expected {
|
|
assert.Assert(t, is.Contains(finalConfigs, cannedConfigRefs[expected]))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpdateCredSpec(t *testing.T) {
|
|
type testCase struct {
|
|
// name is the name of the subtest
|
|
name string
|
|
// flagVal is the value we're setting flagCredentialSpec to
|
|
flagVal string
|
|
// spec is the existing serviceSpec with its configs
|
|
spec *swarm.ContainerSpec
|
|
// expected is the expected value of the credential spec after the
|
|
// function. it may be nil
|
|
expected *swarm.CredentialSpec
|
|
}
|
|
|
|
testCases := []testCase{
|
|
{
|
|
name: "add file credential spec",
|
|
flagVal: "file://somefile",
|
|
spec: &swarm.ContainerSpec{},
|
|
expected: &swarm.CredentialSpec{File: "somefile"},
|
|
}, {
|
|
name: "remove a file credential spec",
|
|
flagVal: "",
|
|
spec: &swarm.ContainerSpec{
|
|
Privileges: &swarm.Privileges{
|
|
CredentialSpec: &swarm.CredentialSpec{
|
|
File: "someFile",
|
|
},
|
|
},
|
|
},
|
|
expected: nil,
|
|
}, {
|
|
name: "remove when no CredentialSpec exists",
|
|
flagVal: "",
|
|
spec: &swarm.ContainerSpec{},
|
|
expected: nil,
|
|
}, {
|
|
name: "add a config credenital spec",
|
|
flagVal: "config://someConfigName",
|
|
spec: &swarm.ContainerSpec{
|
|
Configs: []*swarm.ConfigReference{
|
|
{
|
|
ConfigName: "someConfigName",
|
|
ConfigID: "someConfigID",
|
|
Runtime: &swarm.ConfigReferenceRuntimeTarget{},
|
|
},
|
|
},
|
|
},
|
|
expected: &swarm.CredentialSpec{
|
|
Config: "someConfigID",
|
|
},
|
|
}, {
|
|
name: "remove a config credential spec",
|
|
flagVal: "",
|
|
spec: &swarm.ContainerSpec{
|
|
Privileges: &swarm.Privileges{
|
|
CredentialSpec: &swarm.CredentialSpec{
|
|
Config: "someConfigID",
|
|
},
|
|
},
|
|
},
|
|
expected: nil,
|
|
}, {
|
|
name: "update a config credential spec",
|
|
flagVal: "config://someConfigName",
|
|
spec: &swarm.ContainerSpec{
|
|
Configs: []*swarm.ConfigReference{
|
|
{
|
|
ConfigName: "someConfigName",
|
|
ConfigID: "someConfigID",
|
|
Runtime: &swarm.ConfigReferenceRuntimeTarget{},
|
|
},
|
|
},
|
|
Privileges: &swarm.Privileges{
|
|
CredentialSpec: &swarm.CredentialSpec{
|
|
Config: "someDifferentConfigID",
|
|
},
|
|
},
|
|
},
|
|
expected: &swarm.CredentialSpec{
|
|
Config: "someConfigID",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
flags := newUpdateCommand(nil).Flags()
|
|
flags.Set(flagCredentialSpec, tc.flagVal)
|
|
|
|
updateCredSpecConfig(flags, tc.spec)
|
|
// handle the case where tc.spec.Privileges is nil
|
|
if tc.expected == nil {
|
|
assert.Assert(t, tc.spec.Privileges == nil || tc.spec.Privileges.CredentialSpec == nil)
|
|
return
|
|
}
|
|
|
|
assert.Assert(t, tc.spec.Privileges != nil)
|
|
assert.DeepEqual(t, tc.spec.Privileges.CredentialSpec, tc.expected)
|
|
})
|
|
}
|
|
}
|