Skip to content

Commit e804317

Browse files
committed
multus client: add the multus-cni REST client pkg
Add a pkg to contact the multus RESTful API listening to a UNIX socket. This multus client is not invoked; this will happen in follow-up commits. Signed-off-by: Miguel Duarte Barroso <[email protected]>
1 parent bc29ee4 commit e804317

File tree

6 files changed

+289
-10
lines changed

6 files changed

+289
-10
lines changed

cmd/dynamic-networks-controller/networks-controller.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/maiqueb/multus-dynamic-networks-controller/pkg/cri"
2626
"github.com/maiqueb/multus-dynamic-networks-controller/pkg/cri/containerd"
2727
"github.com/maiqueb/multus-dynamic-networks-controller/pkg/logging"
28+
"github.com/maiqueb/multus-dynamic-networks-controller/pkg/multuscni"
2829
)
2930

3031
const (
@@ -96,11 +97,10 @@ func newController(stopChannel chan struct{}, configuration *config.Multus) (*co
9697
nadInformerFactory,
9798
eventBroadcaster,
9899
newEventRecorder(eventBroadcaster),
99-
configuration.MultusSocketPath,
100100
k8sClient,
101101
nadClientSet,
102102
containerRuntime,
103-
)
103+
multuscni.NewClient(configuration.MultusSocketPath))
104104
if err != nil {
105105
return nil, fmt.Errorf("failed to create the pod networks controller: %v", err)
106106
}

pkg/controller/pod.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/maiqueb/multus-dynamic-networks-controller/pkg/annotations"
2727
"github.com/maiqueb/multus-dynamic-networks-controller/pkg/cri"
2828
"github.com/maiqueb/multus-dynamic-networks-controller/pkg/logging"
29+
"github.com/maiqueb/multus-dynamic-networks-controller/pkg/multuscni"
2930
)
3031

3132
const (
@@ -65,9 +66,9 @@ type PodNetworksController struct {
6566
broadcaster record.EventBroadcaster
6667
recorder record.EventRecorder
6768
workqueue workqueue.RateLimitingInterface
68-
multusSocketPath string
6969
nadClientSet nadclient.Interface
7070
containerRuntime cri.ContainerRuntime
71+
multusClient multuscni.Client
7172
}
7273

7374
// NewPodNetworksController returns new PodNetworksController instance
@@ -76,10 +77,10 @@ func NewPodNetworksController(
7677
nadInformers nadinformers.SharedInformerFactory,
7778
broadcaster record.EventBroadcaster,
7879
recorder record.EventRecorder,
79-
multusSocketPath string,
8080
k8sClientSet kubernetes.Interface,
8181
nadClientSet nadclient.Interface,
8282
containerRuntime cri.ContainerRuntime,
83+
multusClient multuscni.Client,
8384
) (*PodNetworksController, error) {
8485
podInformer := k8sCoreInformerFactory.Core().V1().Pods().Informer()
8586
nadInformer := nadInformers.K8sCniCncfIo().V1().NetworkAttachmentDefinitions().Informer()
@@ -93,13 +94,13 @@ func NewPodNetworksController(
9394
netAttachDefLister: nadInformers.K8sCniCncfIo().V1().NetworkAttachmentDefinitions().Lister(),
9495
recorder: recorder,
9596
broadcaster: broadcaster,
96-
multusSocketPath: multusSocketPath,
9797
workqueue: workqueue.NewNamedRateLimitingQueue(
9898
workqueue.DefaultControllerRateLimiter(),
9999
AdvertisedName),
100100
k8sClientSet: k8sClientSet,
101101
nadClientSet: nadClientSet,
102102
containerRuntime: containerRuntime,
103+
multusClient: multusClient,
103104
}
104105

105106
podInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{

pkg/controller/pod_test.go

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
. "github.com/onsi/ginkgo"
1313
. "github.com/onsi/gomega"
1414

15+
cni100 "github.com/containernetworking/cni/pkg/types/100"
16+
1517
corev1 "k8s.io/api/core/v1"
1618
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1719
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -25,9 +27,12 @@ import (
2527
nadclient "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/client/clientset/versioned"
2628
fakenadclient "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/client/clientset/versioned/fake"
2729
nadinformers "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/client/informers/externalversions"
30+
multusapi "gopkg.in/k8snetworkplumbingwg/multus-cni.v3/pkg/server/api"
2831

2932
"github.com/maiqueb/multus-dynamic-networks-controller/pkg/cri"
3033
fakecri "github.com/maiqueb/multus-dynamic-networks-controller/pkg/cri/fake"
34+
"github.com/maiqueb/multus-dynamic-networks-controller/pkg/multuscni"
35+
fakemultusclient "github.com/maiqueb/multus-dynamic-networks-controller/pkg/multuscni/fake"
3136
)
3237

3338
func TestController(t *testing.T) {
@@ -61,6 +66,7 @@ var _ = Describe("Dynamic Attachment controller", func() {
6166
Context("with an existing running pod", func() {
6267
const (
6368
cniVersion = "0.3.0"
69+
macAddr = "02:03:04:05:06:07"
6470
namespace = "default"
6571
networkName = "tiny-net"
6672
podName = "tiny-winy-pod"
@@ -98,8 +104,10 @@ var _ = Describe("Dynamic Attachment controller", func() {
98104
nadClient,
99105
stopChannel,
100106
eventRecorder,
101-
"",
102107
fakecri.NewFakeRuntime(*pod),
108+
fakemultusclient.NewFakeClient(
109+
networkConfig("ADD", "net1", networkName, macAddr),
110+
networkConfig("DEL", "net0", "", "")),
103111
)).NotTo(BeNil())
104112
Expect(func() []nad.NetworkStatus {
105113
updatedPod, err := k8sClient.CoreV1().Pods(namespace).Get(context.TODO(), podName, metav1.GetOptions{})
@@ -157,6 +165,21 @@ var _ = Describe("Dynamic Attachment controller", func() {
157165
})
158166
})
159167

168+
func networkConfig(cmd, ifaceName, networkName, mac string) fakemultusclient.NetworkConfig {
169+
const cniVersion = "0.4.0"
170+
return fakemultusclient.NetworkConfig{
171+
Cmd: cmd,
172+
IfaceName: ifaceName,
173+
Response: &multusapi.Response{
174+
Result: &cni100.Result{
175+
CNIVersion: cniVersion,
176+
Interfaces: []*cni100.Interface{
177+
{Name: networkName, Mac: mac},
178+
},
179+
}},
180+
}
181+
}
182+
160183
type dummyPodController struct {
161184
*PodNetworksController
162185
networkCache cache.Store
@@ -168,8 +191,8 @@ func newDummyPodController(
168191
nadClient nadclient.Interface,
169192
stopChannel chan struct{},
170193
recorder record.EventRecorder,
171-
cniConfigPath string,
172-
containerRuntime cri.ContainerRuntime) (*dummyPodController, error) {
194+
containerRuntime cri.ContainerRuntime,
195+
multusClient multuscni.Client) (*dummyPodController, error) {
173196
const noResyncPeriod = 0
174197
netAttachDefInformerFactory := nadinformers.NewSharedInformerFactory(nadClient, noResyncPeriod)
175198
podInformerFactory := v1coreinformerfactory.NewSharedInformerFactory(k8sClient, noResyncPeriod)
@@ -179,10 +202,10 @@ func newDummyPodController(
179202
netAttachDefInformerFactory,
180203
nil,
181204
recorder,
182-
cniConfigPath,
183205
k8sClient,
184206
nadClient,
185-
containerRuntime)
207+
containerRuntime,
208+
multusClient)
186209

187210
alwaysReady := func() bool { return true }
188211
podController.arePodsSynched = alwaysReady

pkg/multuscni/client.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package multuscni
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"net"
10+
"net/http"
11+
12+
"k8s.io/klog/v2"
13+
14+
multusapi "gopkg.in/k8snetworkplumbingwg/multus-cni.v3/pkg/server/api"
15+
)
16+
17+
func MultusDelegateURL() string {
18+
const delegateEndpoint = "/delegate"
19+
return multusapi.GetAPIEndpoint(delegateEndpoint)
20+
}
21+
22+
type Client interface {
23+
InvokeDelegate(req *multusapi.Request) (*multusapi.Response, error)
24+
}
25+
26+
type HTTPClient struct {
27+
httpClient *http.Client
28+
serverURL string
29+
}
30+
31+
func NewClient(socketPath string) *HTTPClient {
32+
return &HTTPClient{
33+
httpClient: &http.Client{
34+
Transport: &http.Transport{
35+
DialContext: func(ctx context.Context, network string, addr string) (net.Conn, error) {
36+
return net.Dial("unix", socketPath)
37+
},
38+
},
39+
},
40+
serverURL: MultusDelegateURL(),
41+
}
42+
}
43+
44+
func (c *HTTPClient) InvokeDelegate(req *multusapi.Request) (*multusapi.Response, error) {
45+
httpResp, err := c.DoCNI(req)
46+
if err != nil {
47+
return nil, err
48+
}
49+
response := &multusapi.Response{}
50+
if len(httpResp) != 0 {
51+
if err = json.Unmarshal(httpResp, response); err != nil {
52+
return nil, fmt.Errorf("failed to unmarshal response '%s': %v", string(httpResp), err)
53+
}
54+
}
55+
return response, nil
56+
}
57+
58+
func (c *HTTPClient) DoCNI(req *multusapi.Request) ([]byte, error) {
59+
data, err := json.Marshal(req)
60+
if err != nil {
61+
return nil, fmt.Errorf("failed to marshal CNI request %v: %v", req, err)
62+
}
63+
64+
request, err := httpRequest(c.serverURL, data)
65+
if err != nil {
66+
return nil, err
67+
}
68+
resp, err := c.httpClient.Do(request)
69+
70+
if err != nil {
71+
return nil, fmt.Errorf("failed to send CNI request: %v", err)
72+
}
73+
defer func() {
74+
if err = resp.Body.Close(); err != nil {
75+
klog.Errorf("failed closing the connection to the multus-server: %v", err)
76+
}
77+
}()
78+
79+
body, err := io.ReadAll(resp.Body)
80+
if err != nil {
81+
return nil, fmt.Errorf("failed to read CNI result: %v", err)
82+
}
83+
84+
if resp.StatusCode != http.StatusOK {
85+
return nil, fmt.Errorf("CNI request failed with status %v: '%s'", resp.StatusCode, string(body))
86+
}
87+
88+
return body, nil
89+
}
90+
91+
func httpRequest(serverURL string, payload []byte) (*http.Request, error) {
92+
httpReq, err := http.NewRequestWithContext(context.Background(), http.MethodPost, serverURL, bytes.NewBuffer(payload))
93+
if err != nil {
94+
return nil, err
95+
}
96+
httpReq.Header.Set("Content-Type", "application/json")
97+
return httpReq, nil
98+
}

pkg/multuscni/client_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package multuscni
2+
3+
import (
4+
"encoding/json"
5+
"net"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
10+
. "github.com/onsi/ginkgo/v2"
11+
. "github.com/onsi/gomega"
12+
13+
cni100 "github.com/containernetworking/cni/pkg/types/100"
14+
15+
multusapi "gopkg.in/k8snetworkplumbingwg/multus-cni.v3/pkg/server/api"
16+
)
17+
18+
func TestController(t *testing.T) {
19+
RegisterFailHandler(Fail)
20+
RunSpecs(t, "Dynamic network attachment controller suite")
21+
}
22+
23+
var _ = Describe("multuscni REST client", func() {
24+
const (
25+
cniVersion = "0.4.0"
26+
networkName = "net1"
27+
podIP = "192.168.14.14/24"
28+
podMAC = "02:03:04:05:06:07"
29+
)
30+
It("errors when the server replies with anything other than 200 OK status", func() {
31+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
32+
w.WriteHeader(http.StatusBadRequest)
33+
_, _ = w.Write([]byte(`kablewit`))
34+
}))
35+
36+
defer server.Close()
37+
_, err := newDummyClient(server.Client(), server.URL).InvokeDelegate(multusRequest())
38+
Expect(err).To(MatchError("CNI request failed with status 400: 'kablewit'"))
39+
})
40+
41+
It("errors when the service replies with anything other than a `multusapi.Response` structure", func() {
42+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
43+
w.WriteHeader(http.StatusOK)
44+
_, _ = w.Write([]byte(`{asd:123}`))
45+
}))
46+
47+
defer server.Close()
48+
_, err := newDummyClient(server.Client(), server.URL).InvokeDelegate(multusRequest())
49+
Expect(err).To(MatchError(ContainSubstring("failed to unmarshal response '{asd:123}':")))
50+
})
51+
52+
DescribeTable("return the expected response", func(response *multusapi.Response) {
53+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
54+
w.WriteHeader(http.StatusOK)
55+
serializedResponse, _ := json.Marshal(response)
56+
_, _ = w.Write(serializedResponse)
57+
}))
58+
59+
defer server.Close()
60+
Expect(newDummyClient(server.Client(), server.URL).InvokeDelegate(multusRequest())).To(Equal(response))
61+
},
62+
Entry(
63+
"when the server replies with a simple L2 CNI result",
64+
&multusapi.Response{
65+
Result: &cni100.Result{
66+
CNIVersion: cniVersion,
67+
Interfaces: []*cni100.Interface{
68+
cniInterface(networkName, podMAC),
69+
},
70+
}},
71+
),
72+
Entry("when the server replies with a CNI result featuring IPs", &multusapi.Response{
73+
Result: &cni100.Result{
74+
CNIVersion: cniVersion,
75+
Interfaces: []*cni100.Interface{
76+
cniInterface(networkName, podMAC),
77+
},
78+
IPs: []*cni100.IPConfig{cniIPConfig(podIP)},
79+
},
80+
}),
81+
)
82+
})
83+
84+
func multusRequest() *multusapi.Request {
85+
return &multusapi.Request{
86+
Env: map[string]string{},
87+
Config: nil,
88+
}
89+
}
90+
91+
func newDummyClient(httpClient *http.Client, serverURL string) *HTTPClient {
92+
return &HTTPClient{httpClient: httpClient, serverURL: serverURL}
93+
}
94+
95+
func cniInterface(networkName, macAddress string) *cni100.Interface {
96+
return &cni100.Interface{
97+
Name: networkName,
98+
Mac: macAddress,
99+
}
100+
}
101+
102+
func cniIPConfig(ipStr string) *cni100.IPConfig {
103+
ip, ipNet, _ := net.ParseCIDR(ipStr)
104+
ipNet.IP = ip
105+
return &cni100.IPConfig{
106+
Address: *ipNet,
107+
}
108+
}

0 commit comments

Comments
 (0)