Skip to content

Commit ef07baf

Browse files
committed
mock: act as proxy
By running the mock driver in proxy mode with the csi.sock made available via another *listening* endpoint it becomes possible to run the actual driver code elsewhere, for example embedded inside an e2e.test binary. The second endpoint can be a TCP port that can be reached from outside of a Kubernetes cluster, either via a service or port-forwarding. The same can be done with "socat UNIX-LISTEN:/csi/csi.sock,fork TCP-LISTEN:9000,reuseport", but the implementation inside the mock driver has some advantages compared to that: - the same image as for normal mock driver testing can be used - the mock driver keeps the second listen socket open at all times, so a client connecting to it doesn't get "connection refused" errors, which happens while socat hasn't forked to accept such a connection
1 parent 701d229 commit ef07baf

File tree

4 files changed

+347
-44
lines changed

4 files changed

+347
-44
lines changed

cmd/mock-driver/main.go

Lines changed: 35 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@ limitations under the License.
1616
package main
1717

1818
import (
19+
"context"
1920
"flag"
20-
"fmt"
2121
"io/ioutil"
22-
"net"
2322
"os"
2423
"os/signal"
25-
"strings"
2624
"syscall"
2725

2826
"github.com/kubernetes-csi/csi-test/v4/driver"
27+
"github.com/kubernetes-csi/csi-test/v4/internal/endpoint"
28+
"github.com/kubernetes-csi/csi-test/v4/internal/proxy"
2929
"github.com/kubernetes-csi/csi-test/v4/mock/service"
3030
"gopkg.in/yaml.v2"
3131
"k8s.io/klog/v2"
@@ -50,13 +50,37 @@ func main() {
5050
flag.BoolVar(&config.DisableOnlineExpansion, "disable-online-expansion", false, "Disables online volume expansion capability.")
5151
flag.BoolVar(&config.PermissiveTargetPath, "permissive-target-path", false, "Allows the CO to create PublishVolumeRequest.TargetPath, which violates the CSI spec.")
5252
flag.StringVar(&hooksFile, "hooks-file", "", "YAML file with hook scripts.")
53+
proxyEndpoint := flag.String("proxy-endpoint", "", "Instead of running the CSI driver code, just proxy connections from $CSI_ENDPOINT to the given listening socket.")
5354
flag.Parse()
5455

55-
endpoint := os.Getenv("CSI_ENDPOINT")
56+
csiEndpoint := os.Getenv("CSI_ENDPOINT")
5657
controllerEndpoint := os.Getenv("CSI_CONTROLLER_ENDPOINT")
5758
if len(controllerEndpoint) == 0 {
5859
// If empty, set to the common endpoint.
59-
controllerEndpoint = endpoint
60+
controllerEndpoint = csiEndpoint
61+
}
62+
63+
if *proxyEndpoint != "" {
64+
ctx, cancel := context.WithCancel(context.Background())
65+
defer cancel()
66+
closer, err := proxy.Run(ctx, csiEndpoint, *proxyEndpoint)
67+
if err != nil {
68+
klog.Fatalf("failed to run proxy: %v", err)
69+
}
70+
defer closer.Close()
71+
72+
// Wait for signal
73+
sigc := make(chan os.Signal, 1)
74+
sigs := []os.Signal{
75+
syscall.SIGTERM,
76+
syscall.SIGHUP,
77+
syscall.SIGINT,
78+
syscall.SIGQUIT,
79+
}
80+
signal.Notify(sigc, sigs...)
81+
82+
<-sigc
83+
return
6084
}
6185

6286
if hooksFile != "" {
@@ -71,7 +95,7 @@ func main() {
7195
// Create mock driver
7296
s := service.New(config)
7397

74-
if endpoint == controllerEndpoint {
98+
if csiEndpoint == controllerEndpoint {
7599
servers := &driver.CSIDriverServers{
76100
Controller: s,
77101
Identity: s,
@@ -86,10 +110,10 @@ func main() {
86110
}
87111

88112
// Listen
89-
l, cleanup, err := listen(endpoint)
113+
l, cleanup, err := endpoint.Listen(csiEndpoint)
90114
if err != nil {
91115
klog.Exitf("Error: Unable to listen on %s socket: %v\n",
92-
endpoint,
116+
csiEndpoint,
93117
err)
94118
}
95119
defer cleanup()
@@ -134,7 +158,7 @@ func main() {
134158
}
135159

136160
// Listen controller.
137-
l, cleanupController, err := listen(controllerEndpoint)
161+
l, cleanupController, err := endpoint.Listen(controllerEndpoint)
138162
if err != nil {
139163
klog.Exitf("Error: Unable to listen on %s socket: %v\n",
140164
controllerEndpoint,
@@ -150,10 +174,10 @@ func main() {
150174
klog.Infof("mock controller driver started")
151175

152176
// Listen node.
153-
l, cleanupNode, err := listen(endpoint)
177+
l, cleanupNode, err := endpoint.Listen(csiEndpoint)
154178
if err != nil {
155179
klog.Exitf("Error: Unable to listen on %s socket: %v\n",
156-
endpoint,
180+
csiEndpoint,
157181
err)
158182
}
159183
defer cleanupNode()
@@ -182,39 +206,6 @@ func main() {
182206
}
183207
}
184208

185-
func parseEndpoint(ep string) (string, string, error) {
186-
if strings.HasPrefix(strings.ToLower(ep), "unix://") || strings.HasPrefix(strings.ToLower(ep), "tcp://") {
187-
s := strings.SplitN(ep, "://", 2)
188-
if s[1] != "" {
189-
return s[0], s[1], nil
190-
}
191-
return "", "", fmt.Errorf("Invalid endpoint: %v", ep)
192-
}
193-
// Assume everything else is a file path for a Unix Domain Socket.
194-
return "unix", ep, nil
195-
}
196-
197-
func listen(endpoint string) (net.Listener, func(), error) {
198-
proto, addr, err := parseEndpoint(endpoint)
199-
if err != nil {
200-
return nil, nil, err
201-
}
202-
203-
cleanup := func() {}
204-
if proto == "unix" {
205-
addr = "/" + addr
206-
if err := os.Remove(addr); err != nil && !os.IsNotExist(err) { //nolint: vetshadow
207-
return nil, nil, fmt.Errorf("%s: %q", addr, err)
208-
}
209-
cleanup = func() {
210-
os.Remove(addr)
211-
}
212-
}
213-
214-
l, err := net.Listen(proto, addr)
215-
return l, cleanup, err
216-
}
217-
218209
func parseHooksFile(file string) (*service.Hooks, error) {
219210
var hooks service.Hooks
220211

internal/endpoint/endpoint.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
Copyright 2020 Kubernetes Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package endpoint
18+
19+
import (
20+
"fmt"
21+
"net"
22+
"os"
23+
"strings"
24+
)
25+
26+
func Parse(ep string) (string, string, error) {
27+
if strings.HasPrefix(strings.ToLower(ep), "unix://") || strings.HasPrefix(strings.ToLower(ep), "tcp://") {
28+
s := strings.SplitN(ep, "://", 2)
29+
if s[1] != "" {
30+
return s[0], s[1], nil
31+
}
32+
return "", "", fmt.Errorf("Invalid endpoint: %v", ep)
33+
}
34+
// Assume everything else is a file path for a Unix Domain Socket.
35+
return "unix", ep, nil
36+
}
37+
38+
func Listen(endpoint string) (net.Listener, func(), error) {
39+
proto, addr, err := Parse(endpoint)
40+
if err != nil {
41+
return nil, nil, err
42+
}
43+
44+
cleanup := func() {}
45+
if proto == "unix" {
46+
addr = "/" + addr
47+
if err := os.Remove(addr); err != nil && !os.IsNotExist(err) { //nolint: vetshadow
48+
return nil, nil, fmt.Errorf("%s: %q", addr, err)
49+
}
50+
cleanup = func() {
51+
os.Remove(addr)
52+
}
53+
}
54+
55+
l, err := net.Listen(proto, addr)
56+
return l, cleanup, err
57+
}

internal/proxy/proxy.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package proxy makes it possible to forward a listening socket in
18+
// situations where the proxy cannot connect to some other address.
19+
// Instead, it creates two listening sockets, pairs two incoming
20+
// connections and then moves data back and forth. This matches
21+
// the behavior of the following socat command:
22+
// socat -d -d -d UNIX-LISTEN:/tmp/socat,fork TCP-LISTEN:9000,reuseport
23+
//
24+
// The advantage over that command is that both listening
25+
// sockets are always open, in contrast to the socat solution
26+
// where the TCP port is only open when there actually is a connection
27+
// available.
28+
//
29+
// To establish a connection, someone has to poll the proxy with a dialer.
30+
package proxy
31+
32+
import (
33+
"context"
34+
"fmt"
35+
"io"
36+
"net"
37+
38+
"k8s.io/klog/v2"
39+
40+
"github.com/kubernetes-csi/csi-test/v4/internal/endpoint"
41+
)
42+
43+
// New listens on both endpoints and starts accepting connections
44+
// until closed or the context is done.
45+
func Run(ctx context.Context, endpoint1, endpoint2 string) (io.Closer, error) {
46+
proxy := &proxy{}
47+
failedProxy := proxy
48+
defer func() {
49+
if failedProxy != nil {
50+
failedProxy.Close()
51+
}
52+
}()
53+
54+
proxy.ctx, proxy.cancel = context.WithCancel(ctx)
55+
56+
var err error
57+
proxy.s1, proxy.cleanup1, err = endpoint.Listen(endpoint1)
58+
if err != nil {
59+
return nil, fmt.Errorf("listen %s: %v", endpoint1, err)
60+
}
61+
proxy.s2, proxy.cleanup2, err = endpoint.Listen(endpoint2)
62+
if err != nil {
63+
return nil, fmt.Errorf("listen %s: %v", endpoint2, err)
64+
}
65+
66+
klog.V(3).Infof("proxy listening on %s and %s", endpoint1, endpoint2)
67+
68+
go func() {
69+
for {
70+
// We block on the first listening socket.
71+
// The Linux kernel proactively accepts connections
72+
// on the second one which we will take over below.
73+
conn1 := accept(proxy.ctx, proxy.s1, endpoint1)
74+
if conn1 == nil {
75+
// Done, shut down.
76+
klog.V(5).Infof("proxy endpoint %s closed, shutting down", endpoint1)
77+
return
78+
}
79+
conn2 := accept(proxy.ctx, proxy.s2, endpoint2)
80+
if conn2 == nil {
81+
// Done, shut down. The already accepted
82+
// connection gets closed.
83+
klog.V(5).Infof("proxy endpoint %s closed, shutting down and close established connection", endpoint2)
84+
conn1.Close()
85+
return
86+
}
87+
88+
klog.V(3).Infof("proxy established a new connection between %s and %s", endpoint1, endpoint2)
89+
go copy(conn1, conn2, endpoint1, endpoint2)
90+
go copy(conn2, conn1, endpoint2, endpoint1)
91+
}
92+
}()
93+
94+
failedProxy = nil
95+
return proxy, nil
96+
}
97+
98+
type proxy struct {
99+
ctx context.Context
100+
cancel func()
101+
s1, s2 net.Listener
102+
cleanup1, cleanup2 func()
103+
}
104+
105+
func (p *proxy) Close() error {
106+
if p.cancel != nil {
107+
p.cancel()
108+
}
109+
if p.s1 != nil {
110+
p.s1.Close()
111+
}
112+
if p.s2 != nil {
113+
p.s2.Close()
114+
}
115+
if p.cleanup1 != nil {
116+
p.cleanup1()
117+
}
118+
if p.cleanup2 != nil {
119+
p.cleanup2()
120+
}
121+
return nil
122+
}
123+
124+
func copy(from, to net.Conn, fromEndpoint, toEndpoint string) {
125+
klog.V(5).Infof("starting to copy %s -> %s", fromEndpoint, toEndpoint)
126+
// Signal recipient that no more data is going to come.
127+
// This also stops reading from it.
128+
defer to.Close()
129+
// Copy data until EOF.
130+
cnt, err := io.Copy(to, from)
131+
klog.V(5).Infof("done copying %s -> %s: %d bytes, %v", fromEndpoint, toEndpoint, cnt, err)
132+
}
133+
134+
func accept(ctx context.Context, s net.Listener, endpoint string) net.Conn {
135+
for {
136+
c, err := s.Accept()
137+
if err == nil {
138+
return c
139+
}
140+
// Ignore error if we are shutting down.
141+
if ctx.Err() != nil {
142+
return nil
143+
}
144+
klog.V(3).Infof("accept on %s failed: %v", endpoint, err)
145+
}
146+
}

0 commit comments

Comments
 (0)