Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/command/network/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func newNetworkCommand(dockerCLI command.Cli) *cobra.Command {
newConnectCommand(dockerCLI),
newCreateCommand(dockerCLI),
newDisconnectCommand(dockerCLI),
newEditCommand(dockerCLI),
newInspectCommand(dockerCLI),
newListCommand(dockerCLI),
newRemoveCommand(dockerCLI),
Expand Down
112 changes: 112 additions & 0 deletions cli/command/network/edit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package network

import (
"context"
"fmt"
"io"
"strings"

"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/opts"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
)

type editOptions struct {
labelsToAdd opts.ListOpts
labelsToRm []string
}

func newEditCommand(dockerCLI command.Cli) *cobra.Command {
options := editOptions{
labelsToAdd: opts.NewListOpts(opts.ValidateLabel),
}

cmd := &cobra.Command{
Use: "edit [OPTIONS] NETWORK",
Short: "Edit a network",
Long: `Edit the labels of a network.

Because the Docker Engine API does not support in-place network updates, this
command recreates the network with the same configuration and updated labels.
The network must have no active endpoints before editing; use
'docker network disconnect' to disconnect any containers first.`,
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runEdit(cmd.Context(), dockerCLI.Client(), dockerCLI.Out(), args[0], options)
},
ValidArgsFunction: completion.NetworkNames(dockerCLI),
DisableFlagsInUseLine: true,
}

flags := cmd.Flags()
flags.Var(&options.labelsToAdd, "label-add", "Add or update a label (format: `key=value`)")
flags.StringSliceVar(&options.labelsToRm, "label-rm", nil, "Remove a label by key")
return cmd
}

func runEdit(ctx context.Context, apiClient client.NetworkAPIClient, output io.Writer, networkID string, options editOptions) error {
if options.labelsToAdd.Len() == 0 && len(options.labelsToRm) == 0 {
return fmt.Errorf("no changes requested; use --label-add or --label-rm to modify labels")
}

result, err := apiClient.NetworkInspect(ctx, networkID, client.NetworkInspectOptions{})
if err != nil {
return err
}
nw := result.Network

if len(nw.Containers) > 0 {
var names []string
for _, ep := range nw.Containers {
names = append(names, ep.Name)
}
return fmt.Errorf("network %s has active endpoints (%s); disconnect all containers before editing",
nw.Name, strings.Join(names, ", "))
}

// Build updated labels from existing ones.
labels := make(map[string]string, len(nw.Labels))
for k, v := range nw.Labels {
labels[k] = v
}
for _, l := range options.labelsToAdd.GetSlice() {
k, v, _ := strings.Cut(l, "=")
labels[k] = v
}
for _, k := range options.labelsToRm {
delete(labels, k)
}

// NetworkRemove is called first; if it fails the original network is left intact.
if _, err = apiClient.NetworkRemove(ctx, nw.ID, client.NetworkRemoveOptions{}); err != nil {
return fmt.Errorf("NetworkRemove: %w", err)
}

// Preserve EnableIPv4/EnableIPv6 as pointers so that false values are
// explicitly sent to the daemon (matching the original network's settings).
enableIPv4 := nw.EnableIPv4
enableIPv6 := nw.EnableIPv6

resp, err := apiClient.NetworkCreate(ctx, nw.Name, client.NetworkCreateOptions{
Driver: nw.Driver,
Options: nw.Options,
IPAM: &nw.IPAM,
Internal: nw.Internal,
EnableIPv4: &enableIPv4,
EnableIPv6: &enableIPv6,
Attachable: nw.Attachable,
Ingress: nw.Ingress,
Scope: nw.Scope,
ConfigOnly: nw.ConfigOnly,
ConfigFrom: nw.ConfigFrom.Network,
Labels: labels,
})
if err != nil {
return fmt.Errorf("NetworkCreate: %w", err)
}
_, _ = fmt.Fprintln(output, resp.ID)
return nil
}
135 changes: 135 additions & 0 deletions cli/command/network/edit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package network

import (
"context"
"testing"

"github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
)

func TestNetworkEditNoChanges(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cmd := newEditCommand(cli)
cmd.SetArgs([]string{"mynet"})
err := cmd.Execute()
assert.ErrorContains(t, err, "no changes requested")
}

func TestNetworkEditActiveEndpoints(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
networkInspectFunc: func(_ context.Context, _ string, _ client.NetworkInspectOptions) (client.NetworkInspectResult, error) {
return client.NetworkInspectResult{
Network: network.Inspect{
Network: network.Network{
Name: "mynet",
ID: "abc123",
},
Containers: map[string]network.EndpointResource{
"ep1": {Name: "mycontainer"},
},
},
}, nil
},
})
cmd := newEditCommand(cli)
cmd.SetArgs([]string{"--label-add", "env=prod", "mynet"})
err := cmd.Execute()
assert.ErrorContains(t, err, "active endpoints")
assert.ErrorContains(t, err, "mycontainer")
}

func TestNetworkEditLabelAdd(t *testing.T) {
var createCalled bool
var removeCalled bool

fakeNetwork := network.Inspect{
Network: network.Network{
Name: "mynet",
ID: "abc123",
Driver: "bridge",
Options: map[string]string{},
Labels: map[string]string{"existing": "value"},
},
}

cli := test.NewFakeCli(&fakeClient{
networkInspectFunc: func(_ context.Context, _ string, _ client.NetworkInspectOptions) (client.NetworkInspectResult, error) {
return client.NetworkInspectResult{Network: fakeNetwork}, nil
},
networkRemoveFunc: func(_ context.Context, _ string) error {
removeCalled = true
return nil
},
networkCreateFunc: func(_ context.Context, name string, opts client.NetworkCreateOptions) (client.NetworkCreateResult, error) {
createCalled = true
assert.Equal(t, "mynet", name)
assert.Equal(t, "prod", opts.Labels["env"])
assert.Equal(t, "value", opts.Labels["existing"])
return client.NetworkCreateResult{ID: "newid123"}, nil
},
})

cmd := newEditCommand(cli)
cmd.SetArgs([]string{"--label-add", "env=prod", "mynet"})
assert.NilError(t, cmd.Execute())
assert.Assert(t, removeCalled)
assert.Assert(t, createCalled)
}

func TestNetworkEditLabelRemove(t *testing.T) {
fakeNetwork := network.Inspect{
Network: network.Network{
Name: "mynet",
ID: "abc123",
Driver: "bridge",
Options: map[string]string{},
Labels: map[string]string{"env": "prod", "keep": "me"},
},
}

cli := test.NewFakeCli(&fakeClient{
networkInspectFunc: func(_ context.Context, _ string, _ client.NetworkInspectOptions) (client.NetworkInspectResult, error) {
return client.NetworkInspectResult{Network: fakeNetwork}, nil
},
networkRemoveFunc: func(_ context.Context, _ string) error { return nil },
networkCreateFunc: func(_ context.Context, _ string, opts client.NetworkCreateOptions) (client.NetworkCreateResult, error) {
_, hasEnv := opts.Labels["env"]
assert.Assert(t, !hasEnv, "expected 'env' label to be removed")
assert.Equal(t, "me", opts.Labels["keep"])
return client.NetworkCreateResult{ID: "newid456"}, nil
},
})

cmd := newEditCommand(cli)
cmd.SetArgs([]string{"--label-rm", "env", "mynet"})
assert.NilError(t, cmd.Execute())
}

func TestNetworkEditOutputsNewID(t *testing.T) {
fakeNetwork := network.Inspect{
Network: network.Network{
Name: "mynet",
ID: "abc123",
Driver: "bridge",
Options: map[string]string{},
},
}

cli := test.NewFakeCli(&fakeClient{
networkInspectFunc: func(_ context.Context, _ string, _ client.NetworkInspectOptions) (client.NetworkInspectResult, error) {
return client.NetworkInspectResult{Network: fakeNetwork}, nil
},
networkRemoveFunc: func(_ context.Context, _ string) error { return nil },
networkCreateFunc: func(_ context.Context, _ string, _ client.NetworkCreateOptions) (client.NetworkCreateResult, error) {
return client.NetworkCreateResult{ID: "newid789"}, nil
},
})

cmd := newEditCommand(cli)
cmd.SetArgs([]string{"--label-add", "foo=bar", "mynet"})
assert.NilError(t, cmd.Execute())
assert.Equal(t, "newid789\n", cli.OutBuffer().String())
}