Repository: go-kratos/kratos (go)

Issue:
对于properties类型的namespace，如果namespace名称中含有点号(.)，将会造成panic
#### What happened:
注意namespace名称中含有点号`.`
<img width="1005" alt="image" src="https://user-images.githubusercontent.com/20664531/202617711-ac004a76-4e2d-4f16-9ba5-ce03e77828e5.png">
<img width="1259" alt="image" src="https://user-images.githubusercontent.com/20664531/202617778-8a325a06-b4d7-4e84-9ea5-128a77fd0342.png">

#### How to reproduce it (as minimally and precisely as possible):
创建并使用名称为test.dot的namespace，启动即可复现
```
	c := config.New(
		config.WithSource(
			apollo.NewSource(
				apollo.WithEndpoint("http://config.jidudev.com:8080"),
				apollo.WithNamespace("test.dot"),
				apollo.WithCluster("default"),
				apollo.WithAppID("jidugokitdemo-key")),
		),
	)
```

#### Environment:
- Kratos version (use `kratos -v`): kratos version v2.5.0
- Go version (use `go version`): go version go1.17.13 darwin/arm64
- OS (e.g: `cat /etc/os-release`): mac
- Others:


Pull request description:
fix[config]: fix codec NPE as parsing format wrongly, fix #2517
PTAL

Patch:
diff --git a/contrib/config/apollo/apollo.go b/contrib/config/apollo/apollo.go
index bd96ef11..d463df29 100644
--- a/contrib/config/apollo/apollo.go
+++ b/contrib/config/apollo/apollo.go
@@ -25,6 +25,8 @@ const (
 	properties = "properties"
 )
 
+var formats map[string]struct{}
+
 // Option is apollo option
 type Option func(*options)
 
@@ -129,10 +131,16 @@ func NewSource(opts ...Option) config.Source {
 
 func format(ns string) string {
 	arr := strings.Split(ns, ".")
-	if len(arr) <= 1 || arr[len(arr)-1] == properties {
+	suffix := arr[len(arr)-1]
+	if len(arr) <= 1 || suffix == properties {
+		return json
+	}
+	if _, ok := formats[suffix]; !ok {
+		// fallback
 		return json
 	}
-	return arr[len(arr)-1]
+
+	return suffix
 }
 
 func (e *apollo) load() []*config.KeyValue {
@@ -149,7 +157,7 @@ func (e *apollo) load() []*config.KeyValue {
 			kvs = append(kvs, kv)
 			continue
 		}
-		if strings.Contains(ns, ".") && !strings.Contains(ns, properties) &&
+		if strings.Contains(ns, ".") && !strings.HasSuffix(ns, "."+properties) &&
 			(format(ns) == yaml || format(ns) == yml || format(ns) == json) {
 			kv, err := e.getOriginConfig(ns)
 			if err != nil {
@@ -252,16 +260,28 @@ func resolve(key string, value interface{}, target map[string]interface{}) {
 // eg: namespace.ext with subKey got namespace.subKey
 func genKey(ns, sub string) string {
 	arr := strings.Split(ns, ".")
-	if len(arr) < 1 {
-		return sub
-	}
-
 	if len(arr) == 1 {
 		if ns == "" {
 			return sub
 		}
+
 		return ns + "." + sub
 	}
 
-	return strings.Join(arr[:len(arr)-1], ".") + "." + sub
+	suffix := arr[len(arr)-1]
+	_, ok := formats[suffix]
+	if ok {
+		return strings.Join(arr[:len(arr)-1], ".") + "." + sub
+	}
+
+	return ns + "." + sub
+}
+
+func init() {
+	formats = make(map[string]struct{})
+
+	formats[yaml] = struct{}{}
+	formats[yml] = struct{}{}
+	formats[json] = struct{}{}
+	formats[properties] = struct{}{}
 }
diff --git a/contrib/config/apollo/watcher.go b/contrib/config/apollo/watcher.go
index ea5ef108..d59bdd21 100644
--- a/contrib/config/apollo/watcher.go
+++ b/contrib/config/apollo/watcher.go
@@ -25,7 +25,7 @@ type customChangeListener struct {
 
 func (c *customChangeListener) onChange(namespace string, changes map[string]*storage.ConfigChange) []*config.KeyValue {
 	kv := make([]*config.KeyValue, 0, 2)
-	if strings.Contains(namespace, ".") && !strings.Contains(namespace, properties) &&
+	if strings.Contains(namespace, ".") && !strings.HasSuffix(namespace, "."+properties) &&
 		(format(namespace) == yaml || format(namespace) == yml || format(namespace) == json) {
 		value, err := c.apollo.client.GetConfigCache(namespace).Get("content")
 		if err != nil {


Test patch:
diff --git a/cmd/kratos/internal/base/mod_test.go b/cmd/kratos/internal/base/mod_test.go
index 577896b0..1cf4be96 100644
--- a/cmd/kratos/internal/base/mod_test.go
+++ b/cmd/kratos/internal/base/mod_test.go
@@ -18,8 +18,6 @@ func TestModulePath(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	defer os.RemoveAll("/tmp/test_mod")
-
 	f, err := os.Create("/tmp/test_mod/go.mod")
 	if err != nil {
 		t.Fatal(err)
@@ -38,6 +36,8 @@ go 1.16`
 		t.Fatal(err)
 	}
 	if p != "github.com/go-kratos/kratos/v2" {
-		t.Fatalf("want: %s, got: %s", "module github.com/go-kratos/kratos/v2", p)
+		t.Fatalf("want: %s, got: %s", "github.com/go-kratos/kratos/v2", p)
 	}
+
+	t.Cleanup(func() { os.RemoveAll("/tmp/test_mod") })
 }
diff --git a/contrib/config/apollo/apollo_test.go b/contrib/config/apollo/apollo_test.go
new file mode 100644
index 00000000..185ca4c6
--- /dev/null
+++ b/contrib/config/apollo/apollo_test.go
@@ -0,0 +1,90 @@
+package apollo
+
+import (
+	"testing"
+)
+
+func Test_genKey(t *testing.T) {
+	type args struct {
+		ns  string
+		sub string
+	}
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{
+		{
+			name: "blank namespace",
+			args: args{
+				ns:  "",
+				sub: "x.y",
+			},
+			want: "x.y",
+		},
+		{
+			name: "properties namespace",
+			args: args{
+				ns:  "application",
+				sub: "x.y",
+			},
+			want: "application.x.y",
+		},
+		{
+			name: "namespace with format",
+			args: args{
+				ns:  "app.yaml",
+				sub: "x.y",
+			},
+			want: "app.x.y",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := genKey(tt.args.ns, tt.args.sub); got != tt.want {
+				t.Errorf("genKey() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_format(t *testing.T) {
+	tests := []struct {
+		name      string
+		namespace string
+		want      string
+	}{
+		{
+			name:      "properties namespace",
+			namespace: "application",
+			want:      "json",
+		},
+		{
+			name:      "properties namespace #1",
+			namespace: "app.setting",
+			want:      "json",
+		},
+		{
+			name:      "namespace with format[yaml]",
+			namespace: "app.yaml",
+			want:      "yaml",
+		},
+		{
+			name:      "namespace with format[yml]",
+			namespace: "app.yml",
+			want:      "yml",
+		},
+		{
+			name:      "namespace with format[json]",
+			namespace: "app.json",
+			want:      "json",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := format(tt.namespace); got != tt.want {
+				t.Errorf("format() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}