diff --git a/go.mod b/go.mod index 0af10ae..bcea35f 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( golang.org/x/tools v0.0.0-20200812195022-5ae4c3c160a0 // indirect google.golang.org/grpc v1.35.0 google.golang.org/protobuf v1.25.0 + k8s.io/api v0.20.1 k8s.io/apimachinery v0.20.1 k8s.io/client-go v0.20.1 k8s.io/kubelet v0.20.1 diff --git a/pkg/networkservice/common/createpod/option.go b/pkg/networkservice/common/createpod/option.go new file mode 100644 index 0000000..b830b05 --- /dev/null +++ b/pkg/networkservice/common/createpod/option.go @@ -0,0 +1,45 @@ +// Copyright (c) 2021 Doc.ai and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package createpod + +// Option is an option for the createpod server +type Option func(t *createPodServer) + +// WithNamespace sets namespace in which new pods will be created. Default value is "default". +func WithNamespace(namespace string) Option { + return func(t *createPodServer) { + t.namespace = namespace + } +} + +// WithLabelsKey sets labels key value. Default value is "NSM_LABELS". +// +// Environment variable with specified labels key is set on pod creation, +// to notify the pod about the node it is being create on. +func WithLabelsKey(labelsKey string) Option { + return func(t *createPodServer) { + t.labelsKey = labelsKey + } +} + +// WithNameGenerator sets function to be used for pod name generation. +// Default behavior is to append "-nodeName=" to the template name. +func WithNameGenerator(nameGenerator func(templateName, nodeName string) string) Option { + return func(t *createPodServer) { + t.nameGenerator = nameGenerator + } +} diff --git a/pkg/networkservice/common/createpod/server.go b/pkg/networkservice/common/createpod/server.go new file mode 100644 index 0000000..4991de5 --- /dev/null +++ b/pkg/networkservice/common/createpod/server.go @@ -0,0 +1,92 @@ +// Copyright (c) 2021 Doc.ai and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package createpod provides server chain element that creates pods with specified parameters on demand +package createpod + +import ( + "context" + + "github.com/golang/protobuf/ptypes/empty" + "github.com/networkservicemesh/api/pkg/api/networkservice" + "github.com/networkservicemesh/sdk/pkg/networkservice/core/next" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +const ( + nodeNameKey = "NodeNameKey" +) + +type createPodServer struct { + client kubernetes.Interface + podTemplate *corev1.Pod + namespace string + labelsKey string + nameGenerator func(templateName, nodeName string) string +} + +// NewServer - returns a new server chain element that creates new pods using provided template. +// +// Pods are created on the node with a name specified by key "NodeNameKey" in request labels +// (this label is expected to be filled by clientinfo client). +func NewServer(client kubernetes.Interface, podTemplate *corev1.Pod, options ...Option) networkservice.NetworkServiceServer { + s := &createPodServer{ + podTemplate: podTemplate.DeepCopy(), + client: client, + namespace: "default", + labelsKey: "NSM_LABELS", + nameGenerator: func(templateName, nodeName string) string { + return templateName + "-nodeName=" + nodeName + }, + } + + for _, opt := range options { + opt(s) + } + + return s +} + +func (s *createPodServer) Request(ctx context.Context, request *networkservice.NetworkServiceRequest) (*networkservice.Connection, error) { + nodeName := request.GetConnection().GetLabels()[nodeNameKey] + if nodeName == "" { + return nil, errors.New("NodeNameKey not set") + } + + podTemplate := s.podTemplate.DeepCopy() + podTemplate.ObjectMeta.Name = s.nameGenerator(podTemplate.ObjectMeta.Name, nodeName) + podTemplate.Spec.NodeName = nodeName + for i := range podTemplate.Spec.Containers { + podTemplate.Spec.Containers[i].Env = append(podTemplate.Spec.Containers[i].Env, corev1.EnvVar{ + Name: s.labelsKey, + Value: "nodeName: " + nodeName, + }) + } + + _, err := s.client.CoreV1().Pods(s.namespace).Create(ctx, podTemplate, metav1.CreateOptions{}) + if err != nil { + return nil, errors.WithStack(err) + } + + return nil, errors.New("cannot provide required networkservice") +} + +func (s *createPodServer) Close(ctx context.Context, conn *networkservice.Connection) (*empty.Empty, error) { + return next.Server(ctx).Close(ctx, conn) +} diff --git a/pkg/networkservice/common/createpod/server_test.go b/pkg/networkservice/common/createpod/server_test.go new file mode 100644 index 0000000..16725c5 --- /dev/null +++ b/pkg/networkservice/common/createpod/server_test.go @@ -0,0 +1,151 @@ +// Copyright (c) 2021 Doc.ai and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package createpod_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/networkservicemesh/api/pkg/api/networkservice" + "github.com/networkservicemesh/sdk/pkg/networkservice/common/clientinfo" + "github.com/networkservicemesh/sdk/pkg/networkservice/core/adapters" + "github.com/networkservicemesh/sdk/pkg/networkservice/core/next" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + + "github.com/networkservicemesh/sdk-k8s/pkg/networkservice/common/createpod" +) + +const ( + testNamespace = "pod-ns-name" + nodeName1 = "node1" + nodeName2 = "node2" +) + +func TestCreatePod_RepeatedRequest(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + clientSet := fake.NewSimpleClientset() + + podTemplate := defaultPodTemplate() + + server := next.NewNetworkServiceServer( + adapters.NewClientToServer(clientinfo.NewClient()), + createpod.NewServer(clientSet, podTemplate, createpod.WithNamespace(testNamespace)), + ) + + err := os.Setenv("NODE_NAME", nodeName1) + require.NoError(t, err) + + // first request: should succeed + _, err = server.Request(ctx, &networkservice.NetworkServiceRequest{ + Connection: &networkservice.Connection{}, + }) + require.Error(t, err) + require.Equal(t, "cannot provide required networkservice", err.Error()) + + // second request: should fail + _, err = server.Request(ctx, &networkservice.NetworkServiceRequest{ + Connection: &networkservice.Connection{}, + }) + require.Error(t, err) + require.Equal(t, "pods \""+podTemplate.ObjectMeta.Name+"-nodeName="+nodeName1+"\" already exists", err.Error()) + + podList, err := clientSet.CoreV1().Pods(testNamespace).List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.Equal(t, 1, len(podList.Items)) + pod := podList.Items[0] + + want := podTemplate.DeepCopy() + want.Spec.NodeName = nodeName1 + want.Spec.Containers[0].Env = []corev1.EnvVar{{Name: "NSM_LABELS", Value: "nodeName: " + nodeName1}} + want.Spec.Containers[1].Env = []corev1.EnvVar{{Name: "NSM_LABELS", Value: "nodeName: " + nodeName1}} + require.Equal(t, pod.Spec, want.Spec) + + err = clientSet.CoreV1().Pods(testNamespace).Delete(ctx, pod.ObjectMeta.Name, metav1.DeleteOptions{}) + require.NoError(t, err) + + // third request: should succeed because the pod has died + _, err = server.Request(ctx, &networkservice.NetworkServiceRequest{ + Connection: &networkservice.Connection{}, + }) + require.Error(t, err) + require.Equal(t, "cannot provide required networkservice", err.Error()) +} + +func TestCreatePod_TwoNodes(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + clientSet := fake.NewSimpleClientset() + + podTemplate := defaultPodTemplate() + + server := next.NewNetworkServiceServer( + adapters.NewClientToServer(clientinfo.NewClient()), + createpod.NewServer(clientSet, podTemplate, createpod.WithNamespace(testNamespace)), + ) + + err := os.Setenv("NODE_NAME", nodeName1) + require.NoError(t, err) + + _, err = server.Request(ctx, &networkservice.NetworkServiceRequest{ + Connection: &networkservice.Connection{}, + }) + require.Error(t, err) + require.Equal(t, "cannot provide required networkservice", err.Error()) + + err = os.Setenv("NODE_NAME", nodeName2) + require.NoError(t, err) + + _, err = server.Request(ctx, &networkservice.NetworkServiceRequest{ + Connection: &networkservice.Connection{}, + }) + require.Error(t, err) + require.Equal(t, "cannot provide required networkservice", err.Error()) + + podList, err := clientSet.CoreV1().Pods(testNamespace).List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.Equal(t, 2, len(podList.Items)) + require.Equal(t, nodeName1, podList.Items[0].Spec.NodeName) + require.Equal(t, nodeName2, podList.Items[1].Spec.NodeName) +} + +func defaultPodTemplate() *corev1.Pod { + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "PodName", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "my-container-1", + Image: "my-image-1", + }, + { + Name: "my-container-2", + Image: "my-image-2", + }, + }, + }, + } +}