Skip to content

Commit 5e6ce1b

Browse files
authored
Merge pull request #4856 from thaJeztah/25.0_backport_plugin-socket-tests
[25.0 backport] Add tests for CLI/plugin communication
2 parents 5f1b610 + 1cbc218 commit 5e6ce1b

4 files changed

Lines changed: 494 additions & 1 deletion

File tree

cli-plugins/socket/socket_test.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package socket
2+
3+
import (
4+
"io/fs"
5+
"net"
6+
"os"
7+
"runtime"
8+
"strings"
9+
"testing"
10+
"time"
11+
12+
"gotest.tools/v3/assert"
13+
"gotest.tools/v3/poll"
14+
)
15+
16+
func TestSetupConn(t *testing.T) {
17+
t.Run("updates conn when connected", func(t *testing.T) {
18+
var conn *net.UnixConn
19+
listener, err := SetupConn(&conn)
20+
assert.NilError(t, err)
21+
assert.Check(t, listener != nil, "returned nil listener but no error")
22+
addr, err := net.ResolveUnixAddr("unix", listener.Addr().String())
23+
assert.NilError(t, err, "failed to resolve listener address")
24+
25+
_, err = net.DialUnix("unix", nil, addr)
26+
assert.NilError(t, err, "failed to dial returned listener")
27+
28+
pollConnNotNil(t, &conn)
29+
})
30+
31+
t.Run("allows reconnects", func(t *testing.T) {
32+
var conn *net.UnixConn
33+
listener, err := SetupConn(&conn)
34+
assert.NilError(t, err)
35+
assert.Check(t, listener != nil, "returned nil listener but no error")
36+
addr, err := net.ResolveUnixAddr("unix", listener.Addr().String())
37+
assert.NilError(t, err, "failed to resolve listener address")
38+
39+
otherConn, err := net.DialUnix("unix", nil, addr)
40+
assert.NilError(t, err, "failed to dial returned listener")
41+
42+
otherConn.Close()
43+
44+
_, err = net.DialUnix("unix", nil, addr)
45+
assert.NilError(t, err, "failed to redial listener")
46+
})
47+
48+
t.Run("does not leak sockets to local directory", func(t *testing.T) {
49+
var conn *net.UnixConn
50+
listener, err := SetupConn(&conn)
51+
assert.NilError(t, err)
52+
assert.Check(t, listener != nil, "returned nil listener but no error")
53+
checkDirNoPluginSocket(t)
54+
55+
addr, err := net.ResolveUnixAddr("unix", listener.Addr().String())
56+
assert.NilError(t, err, "failed to resolve listener address")
57+
_, err = net.DialUnix("unix", nil, addr)
58+
assert.NilError(t, err, "failed to dial returned listener")
59+
checkDirNoPluginSocket(t)
60+
})
61+
}
62+
63+
func checkDirNoPluginSocket(t *testing.T) {
64+
t.Helper()
65+
66+
files, err := os.ReadDir(".")
67+
assert.NilError(t, err, "failed to list files in dir to check for leaked sockets")
68+
69+
for _, f := range files {
70+
info, err := f.Info()
71+
assert.NilError(t, err, "failed to check file info")
72+
// check for a socket with `docker_cli_` in the name (from `SetupConn()`)
73+
if strings.Contains(f.Name(), "docker_cli_") && info.Mode().Type() == fs.ModeSocket {
74+
t.Fatal("found socket in a local directory")
75+
}
76+
}
77+
}
78+
79+
func TestConnectAndWait(t *testing.T) {
80+
t.Run("calls cancel func on EOF", func(t *testing.T) {
81+
var conn *net.UnixConn
82+
listener, err := SetupConn(&conn)
83+
assert.NilError(t, err, "failed to setup listener")
84+
85+
done := make(chan struct{})
86+
t.Setenv(EnvKey, listener.Addr().String())
87+
cancelFunc := func() {
88+
done <- struct{}{}
89+
}
90+
ConnectAndWait(cancelFunc)
91+
pollConnNotNil(t, &conn)
92+
conn.Close()
93+
94+
select {
95+
case <-done:
96+
case <-time.After(10 * time.Millisecond):
97+
t.Fatal("cancel function not closed after 10ms")
98+
}
99+
})
100+
101+
// TODO: this test cannot be executed with `t.Parallel()`, due to
102+
// relying on goroutine numbers to ensure correct behaviour
103+
t.Run("connect goroutine exits after EOF", func(t *testing.T) {
104+
var conn *net.UnixConn
105+
listener, err := SetupConn(&conn)
106+
assert.NilError(t, err, "failed to setup listener")
107+
t.Setenv(EnvKey, listener.Addr().String())
108+
numGoroutines := runtime.NumGoroutine()
109+
110+
ConnectAndWait(func() {})
111+
assert.Equal(t, runtime.NumGoroutine(), numGoroutines+1)
112+
113+
pollConnNotNil(t, &conn)
114+
conn.Close()
115+
poll.WaitOn(t, func(t poll.LogT) poll.Result {
116+
if runtime.NumGoroutine() > numGoroutines+1 {
117+
return poll.Continue("waiting for connect goroutine to exit")
118+
}
119+
return poll.Success()
120+
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(10*time.Millisecond))
121+
})
122+
}
123+
124+
func pollConnNotNil(t *testing.T, conn **net.UnixConn) {
125+
t.Helper()
126+
127+
poll.WaitOn(t, func(t poll.LogT) poll.Result {
128+
if *conn == nil {
129+
return poll.Continue("waiting for conn to not be nil")
130+
}
131+
return poll.Success()
132+
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(10*time.Millisecond))
133+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/signal"
7+
"syscall"
8+
"time"
9+
10+
"github.com/docker/cli/cli-plugins/manager"
11+
"github.com/docker/cli/cli-plugins/plugin"
12+
"github.com/docker/cli/cli/command"
13+
"github.com/spf13/cobra"
14+
)
15+
16+
func main() {
17+
plugin.Run(RootCmd, manager.Metadata{
18+
SchemaVersion: "0.1.0",
19+
Vendor: "Docker Inc.",
20+
Version: "test",
21+
})
22+
}
23+
24+
func RootCmd(dockerCli command.Cli) *cobra.Command {
25+
cmd := cobra.Command{
26+
Use: "presocket",
27+
Short: "testing plugin that does not connect to the socket",
28+
// override PersistentPreRunE so that the plugin default
29+
// PersistentPreRunE doesn't run, simulating a plugin built
30+
// with a pre-socket-communication version of the CLI
31+
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
32+
return nil
33+
},
34+
}
35+
36+
cmd.AddCommand(&cobra.Command{
37+
Use: "test-no-socket",
38+
Short: "test command that runs until it receives a SIGINT",
39+
RunE: func(cmd *cobra.Command, args []string) error {
40+
go func() {
41+
<-cmd.Context().Done()
42+
fmt.Fprintln(dockerCli.Out(), "context cancelled")
43+
os.Exit(2)
44+
}()
45+
signalCh := make(chan os.Signal, 10)
46+
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)
47+
go func() {
48+
for range signalCh {
49+
fmt.Fprintln(dockerCli.Out(), "received SIGINT")
50+
}
51+
}()
52+
<-time.After(3 * time.Second)
53+
fmt.Fprintln(dockerCli.Err(), "exit after 3 seconds")
54+
return nil
55+
},
56+
})
57+
58+
cmd.AddCommand(&cobra.Command{
59+
Use: "test-socket",
60+
Short: "test command that runs until it receives a SIGINT",
61+
PreRunE: func(cmd *cobra.Command, args []string) error {
62+
return plugin.PersistentPreRunE(cmd, args)
63+
},
64+
RunE: func(cmd *cobra.Command, args []string) error {
65+
go func() {
66+
<-cmd.Context().Done()
67+
fmt.Fprintln(dockerCli.Out(), "context cancelled")
68+
os.Exit(2)
69+
}()
70+
signalCh := make(chan os.Signal, 10)
71+
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)
72+
go func() {
73+
for range signalCh {
74+
fmt.Fprintln(dockerCli.Out(), "received SIGINT")
75+
}
76+
}()
77+
<-time.After(3 * time.Second)
78+
fmt.Fprintln(dockerCli.Err(), "exit after 3 seconds")
79+
return nil
80+
},
81+
})
82+
83+
cmd.AddCommand(&cobra.Command{
84+
Use: "test-socket-ignore-context",
85+
Short: "test command that runs until it receives a SIGINT",
86+
PreRunE: func(cmd *cobra.Command, args []string) error {
87+
return plugin.PersistentPreRunE(cmd, args)
88+
},
89+
RunE: func(cmd *cobra.Command, args []string) error {
90+
signalCh := make(chan os.Signal, 10)
91+
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)
92+
go func() {
93+
for range signalCh {
94+
fmt.Fprintln(dockerCli.Out(), "received SIGINT")
95+
}
96+
}()
97+
<-time.After(3 * time.Second)
98+
fmt.Fprintln(dockerCli.Err(), "exit after 3 seconds")
99+
return nil
100+
},
101+
})
102+
103+
cmd.AddCommand(&cobra.Command{
104+
Use: "tty",
105+
Short: "test command that attempts to read from the TTY",
106+
RunE: func(cmd *cobra.Command, args []string) error {
107+
done := make(chan struct{})
108+
go func() {
109+
b := make([]byte, 1)
110+
_, _ = dockerCli.In().Read(b)
111+
done <- struct{}{}
112+
}()
113+
select {
114+
case <-done:
115+
case <-time.After(2 * time.Second):
116+
fmt.Fprint(dockerCli.Err(), "timeout after 2 seconds")
117+
}
118+
return nil
119+
},
120+
})
121+
122+
return &cmd
123+
}

0 commit comments

Comments
 (0)