Repository: kubeless/kubeless
Pull request #1165 (2020-09-01 04:45:44)

Added "--service-account" option for function deployment or update
**Issue Ref**: [1162](https://github.com/kubeless/kubeless/issues/1162)

**Description**: 
Added --service-account option for function deployment or update. 
```
kubeless function deploy hellogo --namespace kubeless --runtime go1.12 --from-file helloget.go --handler helloget.Foo --service-account controller-acct
```
```
kubeless function update hellogo --namespace kubeless --runtime go1.12 --from-file helloget.go --handler helloget.Foo --service-account default
```

**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 e9d64d07..069bd48b 100644
--- a/cmd/kubeless/function/deploy.go
+++ b/cmd/kubeless/function/deploy.go
@@ -117,6 +117,11 @@ var deployCmd = &cobra.Command{
 			logrus.Fatal(err)
 		}
 
+		serviceAccount, err := cmd.Flags().GetString("service-account")
+		if err != nil {
+			logrus.Fatal(err)
+		}
+
 		runtimeImage, err := cmd.Flags().GetString("runtime-image")
 		if err != nil {
 			logrus.Fatal(err)
@@ -209,7 +214,7 @@ var deployCmd = &cobra.Command{
 			"function":   funcName,
 		}
 
-		f, err := getFunctionDescription(funcName, ns, handler, file, funcDeps, runtime, runtimeImage, mem, cpu, timeout, imagePullPolicy, port, servicePort, headless, envs, labels, secrets, nodeSelectors, defaultFunctionSpec)
+		f, err := getFunctionDescription(funcName, ns, handler, file, funcDeps, runtime, runtimeImage, mem, cpu, timeout, imagePullPolicy, serviceAccount, port, servicePort, headless, envs, labels, secrets, nodeSelectors, defaultFunctionSpec)
 		if err != nil {
 			logrus.Fatal(err)
 		}
@@ -284,6 +289,7 @@ func init() {
 	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("service-account", "", "", "Specify service account for the function. For example: --service-account controller-acct")
 	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 a6ab5363..6f3db3da 100644
--- a/cmd/kubeless/function/function.go
+++ b/cmd/kubeless/function/function.go
@@ -109,7 +109,7 @@ func parseNodeSelectors(nodeSelectors []string) map[string]string {
 	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) {
+func getFunctionDescription(funcName, ns, handler, file, deps, runtime, runtimeImage, mem, cpu, timeout string, imagePullPolicy string, serviceAccount 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",
@@ -208,6 +208,10 @@ func getFunctionDescription(funcName, ns, handler, file, deps, runtime, runtimeI
 		},
 	}
 
+	if serviceAccount != "" {
+		function.Spec.Deployment.Spec.Template.Spec.ServiceAccountName = serviceAccount
+	}
+
 	if len(defaultFunction.Spec.Deployment.Spec.Template.Spec.Containers) != 0 {
 		function.Spec.Deployment.Spec.Template.Spec.Containers[0].VolumeMounts = defaultFunction.Spec.Deployment.Spec.Template.Spec.Containers[0].VolumeMounts
 	}
diff --git a/cmd/kubeless/function/update.go b/cmd/kubeless/function/update.go
index 66ce6220..f1378d82 100644
--- a/cmd/kubeless/function/update.go
+++ b/cmd/kubeless/function/update.go
@@ -74,6 +74,11 @@ var updateCmd = &cobra.Command{
 			logrus.Fatal(err)
 		}
 
+		serviceAccount, err := cmd.Flags().GetString("service-account")
+		if err != nil {
+			logrus.Fatal(err)
+		}
+
 		runtime, err := cmd.Flags().GetString("runtime")
 		if err != nil {
 			logrus.Fatal(err)
@@ -177,7 +182,7 @@ var updateCmd = &cobra.Command{
 			logrus.Fatal(err)
 		}
 
-		f, err := getFunctionDescription(funcName, ns, handler, file, funcDeps, runtime, runtimeImage, mem, cpu, timeout, imagePullPolicy, port, servicePort, headless, envs, labels, secrets, nodeSelectors, previousFunction)
+		f, err := getFunctionDescription(funcName, ns, handler, file, funcDeps, runtime, runtimeImage, mem, cpu, timeout, imagePullPolicy, serviceAccount, port, servicePort, headless, envs, labels, secrets, nodeSelectors, previousFunction)
 		if err != nil {
 			logrus.Fatal(err)
 		}
@@ -227,6 +232,7 @@ func init() {
 	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("service-account", "", "", "Specify service account for the function. For example: --service-account controller-acct")
 	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")


Test patch:
diff --git a/cmd/kubeless/function/function_test.go b/cmd/kubeless/function/function_test.go
index fdf38697..fe769ff4 100644
--- a/cmd/kubeless/function/function_test.go
+++ b/cmd/kubeless/function/function_test.go
@@ -117,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"}, []string{"foo1=bar1", "baz1:qux1"}, kubelessApi.Function{})
+	result, err := getFunctionDescription("test", "default", "file.handler", file.Name(), "dependencies", "runtime", "test-image", "128Mi", "", "10", "Always", "serviceAccount", 8080, 0, false, []string{"TEST=1"}, []string{"test=1"}, []string{"secretName"}, []string{"foo1=bar1", "baz1:qux1"}, kubelessApi.Function{})
 
 	if err != nil {
 		t.Error(err)
@@ -148,6 +148,7 @@ func TestGetFunctionDescription(t *testing.T) {
 				Spec: appsv1.DeploymentSpec{
 					Template: v1.PodTemplateSpec{
 						Spec: v1.PodSpec{
+							ServiceAccountName: "serviceAccount",
 							Containers: []v1.Container{
 								{
 									Env: []v1.EnvVar{{
@@ -208,7 +209,7 @@ func TestGetFunctionDescription(t *testing.T) {
 	}
 
 	// It should take the default values
-	result2, err := getFunctionDescription("test", "default", "", "", "", "", "", "", "", "", "Always", 8080, 0, false, []string{}, []string{}, []string{}, []string{}, expectedFunction)
+	result2, err := getFunctionDescription("test", "default", "", "", "", "", "", "", "", "", "Always", "", 8080, 0, false, []string{}, []string{}, []string{}, []string{}, expectedFunction)
 
 	if err != nil {
 		t.Error(err)
@@ -229,7 +230,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"}, []string{"foo2=bar2", "baz2:qux2"}, expectedFunction)
+	result3, err := getFunctionDescription("test", "default", "file.handler2", file.Name(), "dependencies2", "runtime2", "test-image2", "256Mi", "100m", "20", "Always", "NewServiceAccount", 8080, 0, false, []string{"TEST=2"}, []string{"test=2"}, []string{"secret2"}, []string{"foo2=bar2", "baz2:qux2"}, expectedFunction)
 
 	if err != nil {
 		t.Error(err)
@@ -260,6 +261,7 @@ func TestGetFunctionDescription(t *testing.T) {
 				Spec: appsv1.DeploymentSpec{
 					Template: v1.PodTemplateSpec{
 						Spec: v1.PodSpec{
+							ServiceAccountName: "NewServiceAccount",
 							Containers: []v1.Container{
 								{
 									Env: []v1.EnvVar{{
@@ -359,7 +361,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{}, []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)
 	}
@@ -368,7 +370,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{}, []string{}, kubelessApi.Function{
+	result5, err := getFunctionDescription("test", "default", "file.handler", file.Name(), "dependencies", "runtime", "test-image", "128Mi", "", "10", "Always", "serviceAccount", 8080, 0, false, []string{"TEST=1"}, []string{"test=1"}, []string{}, []string{}, kubelessApi.Function{
 
 		Spec: kubelessApi.FunctionSpec{
 			HorizontalPodAutoscaler: v2beta1.HorizontalPodAutoscaler{
@@ -383,7 +385,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{}, []string{}, kubelessApi.Function{})
+	result6, err := getFunctionDescription("test", "default", "file.handler", file.Name(), "dependencies", "runtime", "test-image", "128Mi", "", "", "Always", "serviceAccount", 9091, 9092, true, []string{}, []string{}, []string{}, []string{}, kubelessApi.Function{})
 	expectedPort := v1.ServicePort{
 		Name:       "http-function-port",
 		Port:       9092,
@@ -428,6 +430,7 @@ func TestGetFunctionDescription(t *testing.T) {
 				Spec: appsv1.DeploymentSpec{
 					Template: v1.PodTemplateSpec{
 						Spec: v1.PodSpec{
+							ServiceAccountName: "serviceAccount",
 							Containers: []v1.Container{
 								{
 									Env: []v1.EnvVar{{
@@ -484,7 +487,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"}, []string{"foo3=bar3", "baz3:qux3"}, kubelessApi.Function{})
+	result7, err := getFunctionDescription("test", "default", "file.handler", ts.URL, "dependencies", "runtime", "test-image", "128Mi", "", "10", "Always", "serviceAccount", 8080, 0, false, []string{"TEST=1"}, []string{"test=1"}, []string{"secretName"}, []string{"foo3=bar3", "baz3:qux3"}, kubelessApi.Function{})
 
 	if err != nil {
 		t.Error(err)
@@ -508,7 +511,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"}, []string{}, kubelessApi.Function{})
+	result8, err := getFunctionDescription("test", "default", "file.handler", ts2.URL+"/test.zip", "dependencies", "runtime", "test-image", "128Mi", "", "10", "Always", "serviceAccount", 8080, 0, false, []string{"TEST=1"}, []string{"test=1"}, []string{"secretName"}, []string{}, kubelessApi.Function{})
 	if err != nil {
 		t.Error(err)
 	}