Repository: kubeless/kubeless
Pull request #1159 (2020-08-18 12:07:33)

kubeless function deploy/update: add option to specify node selectors.
**Issue Ref**: #1156 
 
**Description**: 

Add a new option `--node-selectors` to the `kubeless function deploy` and `kubeless function update` commands to specify node-selectors for the function.

**TODOs**:
 - [x] Ready to review
 - [x] Automated Tests
 - [ ] Docs



Patch:
diff --git a/cmd/kubeless/function/deploy.go b/cmd/kubeless/function/deploy.go
index 4eddfb0c..b209800d 100644
--- a/cmd/kubeless/function/deploy.go
+++ b/cmd/kubeless/function/deploy.go
@@ -195,13 +195,18 @@ var deployCmd = &cobra.Command{
 			logrus.Fatal("You must specify handler for the runtime.")
 		}
 
+		nodeSelectors, err := cmd.Flags().GetStringSlice("node-selectors")
+		if err != nil {
+			logrus.Fatal(err)
+		}
+
 		defaultFunctionSpec := kubelessApi.Function{}
 		defaultFunctionSpec.ObjectMeta.Labels = map[string]string{
 			"created-by": "kubeless",
 			"function":   funcName,
 		}
 
-		f, err := getFunctionDescription(funcName, ns, handler, file, funcDeps, runtime, runtimeImage, mem, cpu, timeout, imagePullPolicy, port, servicePort, headless, envs, labels, secrets, defaultFunctionSpec)
+		f, err := getFunctionDescription(funcName, ns, handler, file, funcDeps, runtime, runtimeImage, mem, cpu, timeout, imagePullPolicy, port, servicePort, headless, envs, labels, secrets, nodeSelectors, defaultFunctionSpec)
 		if err != nil {
 			logrus.Fatal(err)
 		}
@@ -275,6 +280,7 @@ func init() {
 	deployCmd.Flags().StringSliceP("label", "l", []string{}, "Specify labels of the function. Both separator ':' and '=' are allowed. For example: --label foo1=bar1,foo2:bar2")
 	deployCmd.Flags().StringSliceP("secrets", "", []string{}, "Specify Secrets to be mounted to the functions container. For example: --secrets mySecret")
 	deployCmd.Flags().StringSliceP("env", "e", []string{}, "Specify environment variable of the function. Both separator ':' and '=' are allowed. For example: --env foo1=bar1,foo2:bar2")
+	deployCmd.Flags().StringSliceP("node-selectors", "", []string{}, "Specify node selectors for the function. Both separator ':' and '=' are allowed. For example: --node-selectors key1=val1,key2:val2")
 	deployCmd.Flags().StringP("namespace", "n", "", "Specify namespace for the function")
 	deployCmd.Flags().StringP("dependencies", "d", "", "Specify a file containing list of dependencies for the function")
 	deployCmd.Flags().StringP("schedule", "", "", "Specify schedule in cron format for scheduled function")
diff --git a/cmd/kubeless/function/function.go b/cmd/kubeless/function/function.go
index c7af888c..a6ab5363 100644
--- a/cmd/kubeless/function/function.go
+++ b/cmd/kubeless/function/function.go
@@ -100,8 +100,16 @@ func parseResource(in string) (resource.Quantity, error) {
 	return quantity, nil
 }
 
-func getFunctionDescription(funcName, ns, handler, file, deps, runtime, runtimeImage, mem, cpu, timeout string, imagePullPolicy string, port int32, servicePort int32, headless bool, envs, labels []string, secrets []string, defaultFunction kubelessApi.Function) (*kubelessApi.Function, error) {
+func parseNodeSelectors(nodeSelectors []string) map[string]string {
+	funcNodeSelectors := make(map[string]string)
+	for _, nodeSelector := range nodeSelectors {
+		k, v := getKV(nodeSelector)
+		funcNodeSelectors[k] = v
+	}
+	return funcNodeSelectors
+}
 
+func getFunctionDescription(funcName, ns, handler, file, deps, runtime, runtimeImage, mem, cpu, timeout string, imagePullPolicy string, port int32, servicePort int32, headless bool, envs, labels, secrets, nodeSelectors []string, defaultFunction kubelessApi.Function) (*kubelessApi.Function, error) {
 	function := defaultFunction
 	function.TypeMeta = metav1.TypeMeta{
 		Kind:       "Function",
@@ -245,11 +253,12 @@ func getFunctionDescription(funcName, ns, handler, file, deps, runtime, runtimeI
 
 	}
 
-	selectorLabels := map[string]string{}
-	for k, v := range funcLabels {
-		selectorLabels[k] = v
+	funcNodeSelectors := parseNodeSelectors(nodeSelectors)
+	if len(funcNodeSelectors) == 0 && len(defaultFunction.Spec.Deployment.Spec.Template.Spec.NodeSelector) != 0 {
+		funcNodeSelectors = defaultFunction.Spec.Deployment.Spec.Template.Spec.NodeSelector
 	}
-	selectorLabels["function"] = funcName
+	function.Spec.Deployment.Spec.Template.Spec.NodeSelector = funcNodeSelectors
+
 	return &function, nil
 }
 
diff --git a/cmd/kubeless/function/update.go b/cmd/kubeless/function/update.go
index a2288d4d..498047d0 100644
--- a/cmd/kubeless/function/update.go
+++ b/cmd/kubeless/function/update.go
@@ -164,12 +164,17 @@ var updateCmd = &cobra.Command{
 			logrus.Fatal(err)
 		}
 
+		nodeSelectors, err := cmd.Flags().GetStringSlice("node-selectors")
+		if err != nil {
+			logrus.Fatal(err)
+		}
+
 		previousFunction, err := utils.GetFunction(funcName, ns)
 		if err != nil {
 			logrus.Fatal(err)
 		}
 
-		f, err := getFunctionDescription(funcName, ns, handler, file, funcDeps, runtime, runtimeImage, mem, cpu, timeout, imagePullPolicy, port, servicePort, headless, envs, labels, secrets, previousFunction)
+		f, err := getFunctionDescription(funcName, ns, handler, file, funcDeps, runtime, runtimeImage, mem, cpu, timeout, imagePullPolicy, port, servicePort, headless, envs, labels, secrets, nodeSelectors, previousFunction)
 		if err != nil {
 			logrus.Fatal(err)
 		}
@@ -218,6 +223,7 @@ func init() {
 	updateCmd.Flags().StringSliceP("label", "l", []string{}, "Specify labels of the function")
 	updateCmd.Flags().StringSliceP("secrets", "", []string{}, "Specify Secrets to be mounted to the functions container. For example: --secrets mySecret")
 	updateCmd.Flags().StringSliceP("env", "e", []string{}, "Specify environment variable of the function")
+	updateCmd.Flags().StringSliceP("node-selectors", "", []string{}, "Specify node selectors for the function")
 	updateCmd.Flags().StringP("namespace", "n", "", "Specify namespace for the function")
 	updateCmd.Flags().StringP("dependencies", "d", "", "Specify a file containing list of dependencies for the function")
 	updateCmd.Flags().StringP("runtime-image", "", "", "Custom runtime image")
@@ -228,5 +234,4 @@ func init() {
 	updateCmd.Flags().Int32("servicePort", 0, "Deploy http-based function with a custom service port")
 	updateCmd.Flags().Bool("dryrun", false, "Output JSON manifest of the function without creating it")
 	updateCmd.Flags().StringP("output", "o", "yaml", "Output format")
-
 }
diff --git a/examples/Makefile b/examples/Makefile
index 2e7fedab..11a22645 100644
--- a/examples/Makefile
+++ b/examples/Makefile
@@ -58,7 +58,7 @@ get-python-url-deps-verify:
 	kubeless function call get-python-url-deps |egrep Google	
 
 get-node-url-zip:
-	kubeless function deploy get-node-url-zip --runtime nodejs6 --handler index.helloGet --from-file https://github.com/kubeless/kubeless/blob/master/examples/nodejs/helloFunctions.zip?raw=true
+	kubeless function deploy get-node-url-zip --runtime nodejs10 --handler index.helloGet --from-file https://github.com/kubeless/kubeless/blob/master/examples/nodejs/helloFunctions.zip?raw=true
 
 get-node-url-zip-verify:
 	kubeless function call get-node-url-zip |egrep hello.world
@@ -93,20 +93,20 @@ timeout-python-verify:
 	echo $(MSG) | egrep Request.timeout.exceeded
 
 get-nodejs:
-	kubeless function deploy get-nodejs --runtime nodejs6 --handler helloget.foo --from-file nodejs/helloget.js
+	kubeless function deploy get-nodejs --runtime nodejs10 --handler helloget.foo --from-file nodejs/helloget.js
 
 get-nodejs-verify:
 	kubeless function call get-nodejs |egrep hello.world
 
 get-nodejs-custom-port:
-	kubeless function deploy get-nodejs-custom-port --runtime nodejs6 --handler helloget.foo --from-file nodejs/helloget.js --port 8083
+	kubeless function deploy get-nodejs-custom-port --runtime nodejs10 --handler helloget.foo --from-file nodejs/helloget.js --port 8083
 
 get-nodejs-custom-port-verify:
 	kubectl get svc get-nodejs-custom-port -o yaml | grep 'targetPort: 8083'
 	kubeless function call get-nodejs-custom-port |egrep hello.world
 
 get-nodejs-stream:
-	kubeless function deploy get-nodejs-stream --runtime nodejs6 --handler hellostream.foo --from-file nodejs/hellostream.js --dependencies nodejs/package.json
+	kubeless function deploy get-nodejs-stream --runtime nodejs10 --handler hellostream.foo --from-file nodejs/hellostream.js --dependencies nodejs/package.json
 
 get-nodejs-stream-verify:
 	kubeless function call get-nodejs-stream |egrep hello.world
@@ -114,7 +114,7 @@ get-nodejs-stream-verify:
 timeout-nodejs:
 	$(eval TMPDIR := $(shell mktemp -d))
 	printf 'module.exports = { foo: function (event, context) { while(true) {} } }\n' > $(TMPDIR)/hello-loop.js
-	kubeless function deploy timeout-nodejs --runtime nodejs6 --handler helloget.foo  --from-file $(TMPDIR)/hello-loop.js --timeout 4
+	kubeless function deploy timeout-nodejs --runtime nodejs10 --handler helloget.foo  --from-file $(TMPDIR)/hello-loop.js --timeout 4
 	rm -rf $(TMPDIR)
 
 timeout-nodejs-verify:
@@ -122,14 +122,14 @@ timeout-nodejs-verify:
 	echo $(MSG) | egrep Request.timeout.exceeded
 
 get-nodejs-deps:
-	kubeless function deploy get-nodejs-deps --runtime nodejs6 --handler helloget.handler --from-file nodejs/hellowithdeps.js --dependencies nodejs/package.json
+	kubeless function deploy get-nodejs-deps --runtime nodejs10 --handler helloget.handler --from-file nodejs/hellowithdeps.js --dependencies nodejs/package.json
 
 get-nodejs-deps-verify:
 	kubeless function call get-nodejs-deps --data '{"hello": "world"}' | grep -q 'hello.*world.*date.*UTC'
 
 get-nodejs-multi:
 	cd nodejs; zip helloFunctions.zip *js
-	kubeless function deploy get-nodejs-multi --runtime nodejs6 --handler index.helloGet --from-file nodejs/helloFunctions.zip
+	kubeless function deploy get-nodejs-multi --runtime nodejs10 --handler index.helloGet --from-file nodejs/helloFunctions.zip
 	rm nodejs/helloFunctions.zip
 
 get-nodejs-multi-verify:
@@ -318,7 +318,7 @@ post-python-custom-port-verify:
 	kubeless function call post-python-custom-port --data '{"it-s": "alive"}'|egrep "it.*alive"
 
 post-nodejs:
-	kubeless function deploy post-nodejs --runtime nodejs6 --handler hellowithdata.handler --from-file nodejs/hellowithdata.js
+	kubeless function deploy post-nodejs --runtime nodejs10 --handler hellowithdata.handler --from-file nodejs/hellowithdata.js
 
 post-nodejs-verify:
 	kubeless function call post-nodejs --data '{"it-s": "alive"}'|egrep "it.*alive"
@@ -693,7 +693,7 @@ pubsub-python36-verify:
 
 pubsub-nodejs:
 	kubeless topic create s3-nodejs || true
-	kubeless function deploy pubsub-nodejs --runtime nodejs6 --handler pubsub-nodejs.handler --from-file nodejs/hellowithdata.js
+	kubeless function deploy pubsub-nodejs --runtime nodejs10 --handler pubsub-nodejs.handler --from-file nodejs/hellowithdata.js
 	kubeless trigger kafka create pubsub-nodejs --function-selector created-by=kubeless,function=pubsub-nodejs --trigger-topic s3-nodejs
 
 pubsub-nodejs-verify:


Test patch:
diff --git a/cmd/kubeless/function/function_test.go b/cmd/kubeless/function/function_test.go
index 347492a5..fdf38697 100644
--- a/cmd/kubeless/function/function_test.go
+++ b/cmd/kubeless/function/function_test.go
@@ -89,6 +89,21 @@ func TestParseEnv(t *testing.T) {
 	}
 }
 
+func TestParseNodeSelectors(t *testing.T) {
+	nodeSelectors := []string{
+		"foo=bar",
+		"baz:qux",
+	}
+	expected := map[string]string{
+		"foo": "bar",
+		"baz": "qux",
+	}
+	actual := parseNodeSelectors(nodeSelectors)
+	if eq := reflect.DeepEqual(expected, actual); !eq {
+		t.Errorf("Expect %v got %v", expected, actual)
+	}
+}
+
 func TestGetFunctionDescription(t *testing.T) {
 	// It should parse the given values
 	file, err := ioutil.TempFile("", "test")
@@ -102,7 +117,7 @@ func TestGetFunctionDescription(t *testing.T) {
 	file.Close()
 	defer os.Remove(file.Name()) // clean up
 
-	result, err := getFunctionDescription("test", "default", "file.handler", file.Name(), "dependencies", "runtime", "test-image", "128Mi", "", "10", "Always", 8080, 0, false, []string{"TEST=1"}, []string{"test=1"}, []string{"secretName"}, kubelessApi.Function{})
+	result, err := getFunctionDescription("test", "default", "file.handler", file.Name(), "dependencies", "runtime", "test-image", "128Mi", "", "10", "Always", 8080, 0, false, []string{"TEST=1"}, []string{"test=1"}, []string{"secretName"}, []string{"foo1=bar1", "baz1:qux1"}, kubelessApi.Function{})
 
 	if err != nil {
 		t.Error(err)
@@ -169,6 +184,10 @@ func TestGetFunctionDescription(t *testing.T) {
 									},
 								},
 							},
+							NodeSelector: map[string]string{
+								"foo1": "bar1",
+								"baz1": "qux1",
+							},
 						},
 					},
 				},
@@ -189,7 +208,7 @@ func TestGetFunctionDescription(t *testing.T) {
 	}
 
 	// It should take the default values
-	result2, err := getFunctionDescription("test", "default", "", "", "", "", "", "", "", "", "Always", 8080, 0, false, []string{}, []string{}, []string{}, expectedFunction)
+	result2, err := getFunctionDescription("test", "default", "", "", "", "", "", "", "", "", "Always", 8080, 0, false, []string{}, []string{}, []string{}, []string{}, expectedFunction)
 
 	if err != nil {
 		t.Error(err)
@@ -210,7 +229,7 @@ func TestGetFunctionDescription(t *testing.T) {
 	file.Close()
 	defer os.Remove(file.Name()) // clean up
 
-	result3, err := getFunctionDescription("test", "default", "file.handler2", file.Name(), "dependencies2", "runtime2", "test-image2", "256Mi", "100m", "20", "Always", 8080, 0, false, []string{"TEST=2"}, []string{"test=2"}, []string{"secret2"}, expectedFunction)
+	result3, err := getFunctionDescription("test", "default", "file.handler2", file.Name(), "dependencies2", "runtime2", "test-image2", "256Mi", "100m", "20", "Always", 8080, 0, false, []string{"TEST=2"}, []string{"test=2"}, []string{"secret2"}, []string{"foo2=bar2", "baz2:qux2"}, expectedFunction)
 
 	if err != nil {
 		t.Error(err)
@@ -287,6 +306,10 @@ func TestGetFunctionDescription(t *testing.T) {
 									},
 								},
 							},
+							NodeSelector: map[string]string{
+								"foo2": "bar2",
+								"baz2": "qux2",
+							},
 						},
 					},
 				},
@@ -336,7 +359,7 @@ func TestGetFunctionDescription(t *testing.T) {
 	file.Close()
 	zipW.Close()
 
-	result4, err := getFunctionDescription("test", "default", "file.handler", newfile.Name(), "dependencies", "runtime", "", "", "", "", "Always", 8080, 0, false, []string{}, []string{}, []string{}, expectedFunction)
+	result4, err := getFunctionDescription("test", "default", "file.handler", newfile.Name(), "dependencies", "runtime", "", "", "", "", "Always", 8080, 0, false, []string{}, []string{}, []string{}, []string{}, expectedFunction)
 	if err != nil {
 		t.Error(err)
 	}
@@ -345,7 +368,7 @@ func TestGetFunctionDescription(t *testing.T) {
 	}
 
 	// It should maintain previous HPA definition
-	result5, err := getFunctionDescription("test", "default", "file.handler", file.Name(), "dependencies", "runtime", "test-image", "128Mi", "", "10", "Always", 8080, 0, false, []string{"TEST=1"}, []string{"test=1"}, []string{}, kubelessApi.Function{
+	result5, err := getFunctionDescription("test", "default", "file.handler", file.Name(), "dependencies", "runtime", "test-image", "128Mi", "", "10", "Always", 8080, 0, false, []string{"TEST=1"}, []string{"test=1"}, []string{}, []string{}, kubelessApi.Function{
 
 		Spec: kubelessApi.FunctionSpec{
 			HorizontalPodAutoscaler: v2beta1.HorizontalPodAutoscaler{
@@ -360,7 +383,7 @@ func TestGetFunctionDescription(t *testing.T) {
 	}
 
 	// It should set the Port, ServicePort and headless service properly
-	result6, err := getFunctionDescription("test", "default", "file.handler", file.Name(), "dependencies", "runtime", "test-image", "128Mi", "", "", "Always", 9091, 9092, true, []string{}, []string{}, []string{}, kubelessApi.Function{})
+	result6, err := getFunctionDescription("test", "default", "file.handler", file.Name(), "dependencies", "runtime", "test-image", "128Mi", "", "", "Always", 9091, 9092, true, []string{}, []string{}, []string{}, []string{}, kubelessApi.Function{})
 	expectedPort := v1.ServicePort{
 		Name:       "http-function-port",
 		Port:       9092,
@@ -441,6 +464,10 @@ func TestGetFunctionDescription(t *testing.T) {
 									},
 								},
 							},
+							NodeSelector: map[string]string{
+								"foo3": "bar3",
+								"baz3": "qux3",
+							},
 						},
 					},
 				},
@@ -457,7 +484,7 @@ func TestGetFunctionDescription(t *testing.T) {
 		},
 	}
 
-	result7, err := getFunctionDescription("test", "default", "file.handler", ts.URL, "dependencies", "runtime", "test-image", "128Mi", "", "10", "Always", 8080, 0, false, []string{"TEST=1"}, []string{"test=1"}, []string{"secretName"}, kubelessApi.Function{})
+	result7, err := getFunctionDescription("test", "default", "file.handler", ts.URL, "dependencies", "runtime", "test-image", "128Mi", "", "10", "Always", 8080, 0, false, []string{"TEST=1"}, []string{"test=1"}, []string{"secretName"}, []string{"foo3=bar3", "baz3:qux3"}, kubelessApi.Function{})
 
 	if err != nil {
 		t.Error(err)
@@ -481,7 +508,7 @@ func TestGetFunctionDescription(t *testing.T) {
 
 	expectedURLFunction.Spec.FunctionContentType = "url+zip"
 	expectedURLFunction.Spec.Function = ts2.URL + "/test.zip"
-	result8, err := getFunctionDescription("test", "default", "file.handler", ts2.URL+"/test.zip", "dependencies", "runtime", "test-image", "128Mi", "", "10", "Always", 8080, 0, false, []string{"TEST=1"}, []string{"test=1"}, []string{"secretName"}, kubelessApi.Function{})
+	result8, err := getFunctionDescription("test", "default", "file.handler", ts2.URL+"/test.zip", "dependencies", "runtime", "test-image", "128Mi", "", "10", "Always", 8080, 0, false, []string{"TEST=1"}, []string{"test=1"}, []string{"secretName"}, []string{}, kubelessApi.Function{})
 	if err != nil {
 		t.Error(err)
 	}
@@ -492,5 +519,4 @@ func TestGetFunctionDescription(t *testing.T) {
 		t.Errorf("Unexpected result. Expecting:\n %+v\nReceived:\n %+v", expectedURLFunction, *result8)
 	}
 	// end test
-
 }