Repository: streamnative/pulsarctl (go)

Issue:
cannot marshal secrets passed in yaml file correctly
with below format secrets in a yaml file:

```yaml
---
secrets:
 snowflake:
   path: "devint-snowflake-gmbx"
   key: "password"
 questionPro:
   path: "questionpro-cx-api-key"
   key: "api_key"
```

it will reports below error when creating function:

```bash
❯ pulsarctl functions create --jar function://public/default/java-function@1.0 --name package-java-fn --classname org.apache.pulsar.functions.api.examples.ExclamationFunction --inputs persistent://public/default/package-java-fn-input --output persistent://public/default/java-function@1.0 --cpu 0.1 --function-config-file test.yaml
error: json: unsupported type: map[interface {}]interface {}
[✖]  json: unsupported type: map[interface {}]interface {}
```

Pull request description:
Fix json marshal error for Secrets and UserConfigs when creating/updating functions
<!--
### Contribution Checklist

  - Fill out the template below to describe the changes contributed by the pull request. That will give reviewers the context they need to do the review.
  
  - Each pull request should address only one issue, not mix up code from multiple issues.
  
  - Each commit in the pull request has a meaningful commit message

  - Once all items of the checklist are addressed, remove the above text and this checklist, leaving only the filled out template below.

**(The sections below can be removed for hotfixes of typos)**
-->

*(If this PR fixes a github issue, please add `Fixes #<xyz>`.)*

Fixes #1508

*(or if this PR is one task of a github issue, please add `Master Issue: #<xyz>` to link to the master issue.)*

Master Issue: #<xyz>

### Motivation

golang's json cannot marshal `map[interface {}]interface {}` type struct, we need to convert it to a `map[string]interface{}` first

### Modifications

convert the `map[interface {}]interface {}` type to `map[string]interface {}` type before marshal to json

### Verifying this change

- [x] Make sure that the change passes the CI checks.

- [x] This change added tests and can be verified as follows:

  - *Added integration tests for creating function with a function config file*
  - *Added a unittest to test Secrets and userConfigs in different format*

### Documentation

Check the box below.

Need to update docs? 

- [ ] `doc-required` 
  
  (If you need help on updating docs, create a doc issue)
  
- [x] `no-need-doc` 
  
  (Please explain why)
  
- [ ] `doc` 
  
  (If this PR contains doc changes)



Patch:
diff --git a/.github/workflows/ci-trivy.yml b/.github/workflows/ci-trivy.yml
index ce06e55..95075db 100644
--- a/.github/workflows/ci-trivy.yml
+++ b/.github/workflows/ci-trivy.yml
@@ -33,4 +33,5 @@ jobs:
           image-ref: 'pulsarctl:latest'
           format: 'table'
           exit-code: '1'
-          severity: "MEDIUM,HIGH,CRITICAL"
\ No newline at end of file
+          severity: "MEDIUM,HIGH,CRITICAL"
+          vuln-type: "library"
\ No newline at end of file
diff --git a/pkg/ctl/functions/create.go b/pkg/ctl/functions/create.go
index b6b1c96..2f0593d 100644
--- a/pkg/ctl/functions/create.go
+++ b/pkg/ctl/functions/create.go
@@ -508,6 +508,8 @@ func doCreateFunctions(vc *cmdutils.VerbCmd, funcData *util.FunctionData) error
 		return err
 	}
 
+	formatFuncConf(funcData.FuncConf)
+
 	admin := cmdutils.NewPulsarClientWithAPIVersion(config.V3)
 
 	if utils.IsPackageURLSupported(funcData.UserCodeFile) {
diff --git a/pkg/ctl/functions/util.go b/pkg/ctl/functions/util.go
index 2f9d274..68efe03 100644
--- a/pkg/ctl/functions/util.go
+++ b/pkg/ctl/functions/util.go
@@ -408,6 +408,38 @@ func validateFunctionConfigs(functionConfig *util.FunctionConfig) error {
 	return nil
 }
 
+// the UserConfig and Secrets fields of FunctionConfig are a map[string]interface{},
+// and the type of value can be a map[interface{}]interface{} which cannot be marshaled by json,
+// so we need to convert the map[interface{}]interface{} to a map[string]interface{}
+func formatFuncConf(funcConf *util.FunctionConfig) {
+	if funcConf == nil {
+		return
+	}
+	for k, v := range funcConf.UserConfig {
+		funcConf.UserConfig[k] = convertMap(v)
+	}
+	for k, v := range funcConf.Secrets {
+		funcConf.Secrets[k] = convertMap(v)
+	}
+}
+
+// convertMap converts a map[interface{}]interface{} to map[string]interface{}
+func convertMap(i interface{}) interface{} {
+	switch x := i.(type) {
+	case map[interface{}]interface{}:
+		m := make(map[string]interface{})
+		for k, v := range x {
+			m[k.(string)] = convertMap(v)
+		}
+		return m
+	case []interface{}:
+		for i, v := range x {
+			x[i] = convertMap(v)
+		}
+	}
+	return i
+}
+
 func processBaseArguments(funcData *util.FunctionData) error {
 	usesSetters := funcData.Tenant != "" || funcData.Namespace != "" || funcData.FuncName != ""
 	usesFqfn := funcData.FQFN != ""


Test patch:
diff --git a/pkg/ctl/functions/create_test.go b/pkg/ctl/functions/create_test.go
index 8d758e3..2d0ceb7 100644
--- a/pkg/ctl/functions/create_test.go
+++ b/pkg/ctl/functions/create_test.go
@@ -57,3 +57,22 @@ func TestCreatePyFunction(t *testing.T) {
 	_, execErr, err := TestFunctionsCommands(createFunctionsCmd, argsWithFileURLPy)
 	FailImmediatelyIfErrorNotNil(t, execErr, err)
 }
+
+func TestCreateFunctionWithConfigFile(t *testing.T) {
+	jarName := path.Join(ResourceDir(), "api-examples.jar")
+	fName := "jf" + test.RandomSuffix()
+	configFile := path.Join(ResourceDir(), "example-function-config.yaml")
+	args := []string{
+		"create",
+		"--tenant", "public",
+		"--namespace", "default",
+		"--name", fName,
+		"--inputs", "test-input-topic",
+		"--output", "persistent://public/default/test-output-topic",
+		"--classname", "org.apache.pulsar.functions.api.examples.ExclamationFunction",
+		"--jar", jarName,
+		"--function-config-file", configFile,
+		"--processing-guarantees", "EFFECTIVELY_ONCE"}
+	_, execErr, err := TestFunctionsCommands(createFunctionsCmd, args)
+	FailImmediatelyIfErrorNotNil(t, execErr, err)
+}
diff --git a/pkg/ctl/functions/util_test.go b/pkg/ctl/functions/util_test.go
new file mode 100644
index 0000000..8b29391
--- /dev/null
+++ b/pkg/ctl/functions/util_test.go
@@ -0,0 +1,58 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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 functions
+
+import (
+	"encoding/json"
+	"os"
+	"path"
+	"strings"
+	"testing"
+
+	util "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils"
+	"github.com/stretchr/testify/assert"
+	"gopkg.in/yaml.v2"
+)
+
+func TestMarshalSecretsAndUserConfig(t *testing.T) {
+	configFile := path.Join(ResourceDir(), "example-function-config.yaml")
+	yamlFile, err := os.ReadFile(configFile)
+	assert.Nil(t, err)
+
+	funcConf := &util.FunctionConfig{}
+	err = yaml.Unmarshal(yamlFile, funcConf)
+	assert.Nil(t, err)
+
+	// should report error when marshal directly
+	_, err = json.Marshal(funcConf)
+	assert.NotNil(t, err)
+	assert.True(t, strings.Contains(err.Error(), "json: unsupported type: map[interface {}]interface {}"))
+
+	// should not report error when marshal after format
+	formatFuncConf(funcConf)
+	jsonData, err := json.Marshal(funcConf)
+	assert.Nil(t, err)
+	expectedResult := `{"cleanupSubscription":false,"retainOrdering":false,"retainKeyOrdering":false,` +
+		`"forwardSourceMessageProperty":false,"autoAck":true,"parallelism":1,"output":"test_result","tenant":"public",` +
+		`"namespace":"default","name":"example-functions-config","className":"org.apache.pulsar.functions.api.examples.` +
+		`ExclamationFunction","inputs":["test_src"],"userConfig":{"arrayMapKey":[{"key":"value2","path":"config2"}],` +
+		`"mapKey":{"key":"value","path":"config"},"stringKey":"stringValue"},"secrets":{"arrayMapKey":` +
+		`[{"key":"password2","path":"secret2"}],"mapKey":{"key":"password","path":"secret"},"stringKey":"stringSecret"},` +
+		`"exposePulsarAdminClientEnabled":false,"skipToLatest":false}`
+	assert.Equal(t, expectedResult, string(jsonData))
+}
diff --git a/test/functions/example-function-config.yaml b/test/functions/example-function-config.yaml
index ff08cad..407d0f1 100644
--- a/test/functions/example-function-config.yaml
+++ b/test/functions/example-function-config.yaml
@@ -25,3 +25,19 @@ inputs: ["test_src"]
 output: "test_result"
 autoAck: true
 parallelism: 1
+secrets:
+  stringKey: "stringSecret"
+  mapKey:
+    path: "secret"
+    key: "password"
+  arrayMapKey:
+    - path: "secret2"
+      key: "password2"
+userConfig:
+  stringKey: "stringValue"
+  mapKey:
+    path: "config"
+    key: "value"
+  arrayMapKey:
+    - path: "config2"
+      key: "value2"