Skip to content

Commit

Permalink
Merge pull request #147 from d-uzlov/feat-141-pod-auto-creation
Browse files Browse the repository at this point in the history
Add createpod element
  • Loading branch information
denis-tingaikin authored May 13, 2021
2 parents 683110c + 9c73ead commit a79ea45
Show file tree
Hide file tree
Showing 4 changed files with 289 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions pkg/networkservice/common/createpod/option.go
Original file line number Diff line number Diff line change
@@ -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=<nodeName>" to the template name.
func WithNameGenerator(nameGenerator func(templateName, nodeName string) string) Option {
return func(t *createPodServer) {
t.nameGenerator = nameGenerator
}
}
92 changes: 92 additions & 0 deletions pkg/networkservice/common/createpod/server.go
Original file line number Diff line number Diff line change
@@ -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)
}
151 changes: 151 additions & 0 deletions pkg/networkservice/common/createpod/server_test.go
Original file line number Diff line number Diff line change
@@ -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",
},
},
},
}
}

0 comments on commit a79ea45

Please sign in to comment.