-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #147 from d-uzlov/feat-141-pod-auto-creation
Add createpod element
- Loading branch information
Showing
4 changed files
with
289 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
}, | ||
}, | ||
}, | ||
} | ||
} |