Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support go-template syntax for pod templates #353

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ linters-settings:
- regexpPattern
- singleCaseSwitch
- sloppyLen
- sloppyReassign
- stringXbytes
- switchTrue
- typeAssertChain
Expand Down
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ require (
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.0
go.uber.org/atomic v1.7.0
go.uber.org/goleak v1.1.12
golang.org/x/tools v0.1.10 // indirect
google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12 // indirect
Expand Down
10 changes: 1 addition & 9 deletions pkg/networkservice/common/createpod/option.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2021 Doc.ai and/or its affiliates.
// Copyright (c) 2021-2022 Doc.ai and/or its affiliates.
//
// SPDX-License-Identifier: Apache-2.0
//
Expand All @@ -25,11 +25,3 @@ func WithNamespace(namespace string) Option {
t.namespace = namespace
}
}

// WithNameGenerator sets function to be used for pod name generation.
// Default behavior is to append a random uuid to the template name.
func WithNameGenerator(nameGenerator func(templateName, nodeName string) string) Option {
return func(t *createPodServer) {
t.nameGenerator = nameGenerator
}
}
75 changes: 54 additions & 21 deletions pkg/networkservice/common/createpod/server.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2021 Doc.ai and/or its affiliates.
// Copyright (c) 2021-2022 Doc.ai and/or its affiliates.
//
// SPDX-License-Identifier: Apache-2.0
//
Expand All @@ -18,14 +18,19 @@
package createpod

import (
"bytes"
"context"
"sync"
"text/template"

"github.com/golang/protobuf/ptypes/empty"
"github.com/google/uuid"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/kubernetes"

Expand All @@ -39,12 +44,12 @@ const (
)

type createPodServer struct {
ctx context.Context
client kubernetes.Interface
podTemplate *corev1.Pod
namespace string
nodeMap nodeInfoMap
nameGenerator func(templateName, nodeName string) string
ctx context.Context
client kubernetes.Interface
podTemplate string
namespace string
nodeMap nodeInfoMap
deserializer runtime.Decoder
}

type nodeInfo struct {
Expand All @@ -56,13 +61,17 @@ type nodeInfo struct {
//
// 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(ctx context.Context, client kubernetes.Interface, podTemplate *corev1.Pod, options ...Option) networkservice.NetworkServiceServer {
func NewServer(ctx context.Context, client kubernetes.Interface, podTemplate string, options ...Option) networkservice.NetworkServiceServer {
scheme := runtime.NewScheme()
codecFactory := serializer.NewCodecFactory(scheme)
deserializer := codecFactory.UniversalDeserializer()

s := &createPodServer{
ctx: ctx,
podTemplate: podTemplate.DeepCopy(),
client: client,
namespace: "default",
nameGenerator: func(templateName, nodeName string) string { return templateName + "-" + uuid.New().String() },
ctx: ctx,
podTemplate: podTemplate,
client: client,
namespace: "default",
deserializer: deserializer,
}

for _, opt := range options {
Expand Down Expand Up @@ -119,23 +128,47 @@ func (s *createPodServer) Request(ctx context.Context, request *networkservice.N
return nil, errors.New("cannot provide required networkservice: local endpoint already exists")
}

ni.name = s.nameGenerator(s.podTemplate.ObjectMeta.Name, nodeName)
err := s.createPod(ctx, nodeName, ni.name)
name, err := s.createPod(ctx, nodeName, request.GetConnection())
if err != nil {
return nil, errors.WithStack(err)
}
ni.name = name
return nil, errors.Errorf("cannot provide required networkservice: local endpoint created as %v", ni.name)
}

func (s *createPodServer) Close(ctx context.Context, conn *networkservice.Connection) (*empty.Empty, error) {
return next.Server(ctx).Close(ctx, conn)
}

func (s *createPodServer) createPod(ctx context.Context, nodeName, podName string) error {
podTemplate := s.podTemplate.DeepCopy()
podTemplate.ObjectMeta.Name = podName
podTemplate.Spec.NodeName = nodeName
func (s *createPodServer) createPod(ctx context.Context, nodeName string, conn *networkservice.Connection) (string, error) {
var t, err = template.New("createPod").Funcs(template.FuncMap{
"uuid": func() string {
return uuid.New().String()
},
}).Parse(s.podTemplate)

if err != nil {
return "", err
}
var buffer bytes.Buffer
if err = t.Execute(&buffer, conn); err != nil {
return "", err
}
var pod corev1.Pod

_, _, err = s.deserializer.Decode(buffer.Bytes(), nil, &pod)
if err != nil {
return "", err
}

if pod.Spec.NodeName == "" {
pod.Spec.NodeName = nodeName
}

resp, err := s.client.CoreV1().Pods(s.namespace).Create(ctx, &pod, metav1.CreateOptions{})
if err != nil {
return "", err
}

_, err := s.client.CoreV1().Pods(s.namespace).Create(ctx, podTemplate, metav1.CreateOptions{})
return err
return resp.GetObjectMeta().GetName(), nil
}
96 changes: 45 additions & 51 deletions pkg/networkservice/common/createpod/server_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// Copyright (c) 2021 Doc.ai and/or its affiliates.
// Copyright (c) 2021-2022 Doc.ai and/or its affiliates.
//
// Copyright (c) 2021-2022 Doc.ai and/or its affiliates.
//
// SPDX-License-Identifier: Apache-2.0
//
Expand All @@ -19,13 +21,10 @@ package createpod_test
import (
"context"
"os"
"strconv"
"testing"
"time"

"github.com/stretchr/testify/require"
"go.uber.org/atomic"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"

Expand All @@ -49,17 +48,10 @@ func TestCreatePod_RepeatedRequest(t *testing.T) {

clientSet := fake.NewSimpleClientset()

podTemplate := defaultPodTemplate()

counter := atomic.Int32{}

server := next.NewNetworkServiceServer(
adapters.NewClientToServer(clientinfo.NewClient()),
createpod.NewServer(ctx, clientSet, podTemplate,
createpod.NewServer(ctx, clientSet, defaultPodTemplate,
createpod.WithNamespace(testNamespace),
createpod.WithNameGenerator(func(templateName, nodeName string) string {
return templateName + strconv.Itoa(int(counter.Add(1)))
}),
),
)

Expand All @@ -71,7 +63,7 @@ func TestCreatePod_RepeatedRequest(t *testing.T) {
Connection: &networkservice.Connection{},
})
require.Error(t, err)
require.Equal(t, "cannot provide required networkservice: local endpoint created as PodName1", err.Error())
require.Contains(t, err.Error(), "cannot provide required networkservice: local endpoint created as ")

// second request: should fail
_, err = server.Request(ctx, &networkservice.NetworkServiceRequest{
Expand All @@ -85,10 +77,6 @@ func TestCreatePod_RepeatedRequest(t *testing.T) {
require.Equal(t, 1, len(podList.Items))
pod := podList.Items[0].DeepCopy()

want := podTemplate.DeepCopy()
want.Spec.NodeName = nodeName1
require.Equal(t, pod.Spec, want.Spec)

pod.Status.Phase = "Succeeded"
_, err = clientSet.CoreV1().Pods(testNamespace).UpdateStatus(ctx, pod, metav1.UpdateOptions{})
require.NoError(t, err)
Expand All @@ -99,7 +87,10 @@ func TestCreatePod_RepeatedRequest(t *testing.T) {
Connection: &networkservice.Connection{},
})
require.Error(t, err)
return err.Error() == "cannot provide required networkservice: local endpoint created as PodName2"

podList, err = clientSet.CoreV1().Pods(testNamespace).List(ctx, metav1.ListOptions{})
require.NoError(t, err)
return len(podList.Items) > 0 && podList.Items[0].GetName() != pod.GetName()
}, time.Millisecond*100, time.Millisecond*10)

podList, err = clientSet.CoreV1().Pods(testNamespace).List(ctx, metav1.ListOptions{})
Expand All @@ -117,61 +108,64 @@ func TestCreatePod_TwoNodes(t *testing.T) {

clientSet := fake.NewSimpleClientset()

podTemplate := defaultPodTemplate()

counter := atomic.Int32{}

server := next.NewNetworkServiceServer(
adapters.NewClientToServer(clientinfo.NewClient()),
createpod.NewServer(ctx, clientSet, podTemplate,
createpod.NewServer(ctx, clientSet, defaultPodTemplate,
createpod.WithNamespace(testNamespace),
createpod.WithNameGenerator(func(templateName, nodeName string) string {
return templateName + strconv.Itoa(int(counter.Add(1)))
}),
),
)

err := os.Setenv("NODE_NAME", nodeName1)
require.NoError(t, err)

_, err = server.Request(ctx, &networkservice.NetworkServiceRequest{
Connection: &networkservice.Connection{},
Connection: &networkservice.Connection{
Labels: map[string]string{
"a": "b",
"c": "e",
},
},
})
require.Error(t, err)
require.Equal(t, "cannot provide required networkservice: local endpoint created as PodName1", err.Error())
require.Contains(t, err.Error(), "cannot provide required networkservice: local endpoint created as ")

err = os.Setenv("NODE_NAME", nodeName2)
require.NoError(t, err)

_, err = server.Request(ctx, &networkservice.NetworkServiceRequest{
Connection: &networkservice.Connection{},
Connection: &networkservice.Connection{
Labels: map[string]string{
"a": "b",
"c": "e",
},
},
})
require.Error(t, err)
require.Equal(t, "cannot provide required networkservice: local endpoint created as PodName2", err.Error())
require.Contains(t, err.Error(), "cannot provide required networkservice: local endpoint created as ")

podList, err := clientSet.CoreV1().Pods(testNamespace).List(ctx, metav1.ListOptions{})
require.NoError(t, err)
var nodesSet = map[string]struct{}{
nodeName1: {},
nodeName2: {},
}
require.Contains(t, podList.Items[0].GetLabels(), "a")
require.Contains(t, podList.Items[0].GetLabels(), "c")
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)
require.Contains(t, nodesSet, podList.Items[0].Spec.NodeName)
delete(nodesSet, podList.Items[0].Spec.NodeName)
require.Contains(t, nodesSet, 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",
},
},
},
}
}
const defaultPodTemplate = `---
apiVersion: apps/v1
kind: Pod
metadata:
name: nse-{{ uuid }}
labels: {{ range $key, $value := .Labels }}
"{{ $key }}": "{{ $value }}"{{ end }}
objectmeta:
containers:
- name: my-container-1
image: my-image-1
`