Repository: mosn/layotto
Pull request #1001 (2023-10-12 03:55:30)

feat: support tencentcloud sms component
<!--  Thanks for sending a pull request!
Read contributing.md before commit pull request.
-->

**What this PR does**:
Make Layotto support tencentcloud sms component for sms API.

**Which issue(s) this PR fixes**:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
-->
Not a fix, but a completion of part of the task for #830.

**Special notes for your reviewer**:

**Does this PR introduce a user-facing change?**:
<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".
-->
```release-note

```

github-actions[bot]: Hi @alilestera. Thanks for your PR! 🎉
If the PR is ready, use the `/cc` command to assign reviewer to review. 

<details>
  <summary>Details</summary>

The full list of commands accepted by this bot can be found [here](https://github.com/Xunzhuo/prowox/blob/main/COMMAND.md).

The pull request process is described [here](https://github.com/Xunzhuo/prowox/blob/main/PROCESS.md).

</details>
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/mosn/layotto/pull/1001?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mosn) Report
All modified lines are covered by tests :white_check_mark:



:loudspeaker: Thoughts on this report? [Let us know!](https://about.codecov.io/pull-request-comment-report/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mosn).

alilestera: /cc @zhenjunMa 
/cc @wenxuwan 

Patch:
diff --git a/cmd/layotto/main.go b/cmd/layotto/main.go
index a27246d8..b96cb156 100644
--- a/cmd/layotto/main.go
+++ b/cmd/layotto/main.go
@@ -23,6 +23,8 @@ import (
 	"strconv"
 	"time"
 
+	"mosn.io/layotto/components/sms"
+
 	"mosn.io/layotto/components/cryption"
 
 	"mosn.io/layotto/components/email"
@@ -46,6 +48,7 @@ import (
 	aliyun_file "mosn.io/layotto/components/file/aliyun"
 
 	aliyun_email "mosn.io/layotto/components/email/aliyun"
+	tencentcloud_sms "mosn.io/layotto/components/sms/tencentcloud"
 
 	"github.com/dapr/components-contrib/secretstores"
 	"github.com/dapr/components-contrib/secretstores/aws/parameterstore"
@@ -320,6 +323,10 @@ func NewRuntimeGrpcServer(data json.RawMessage, opts ...grpc.ServerOption) (mgrp
 		runtime.WithEmailServiceFactory(
 			email.NewFactory("aliyun.email", aliyun_email.NewAliyunEmail),
 		),
+		// Sms
+		runtime.WithSmsServiceFactory(
+			sms.NewFactory("tencentcloud.sms", tencentcloud_sms.NewSms),
+		),
 		// PubSub
 		runtime.WithPubSubFactory(
 			pubsub.NewFactory("redis", func() dapr_comp_pubsub.PubSub {
diff --git a/cmd/layotto_multiple_api/main.go b/cmd/layotto_multiple_api/main.go
index 436157e0..1856926a 100644
--- a/cmd/layotto_multiple_api/main.go
+++ b/cmd/layotto_multiple_api/main.go
@@ -23,9 +23,12 @@ import (
 	"strconv"
 	"time"
 
+	"mosn.io/layotto/components/sms"
+
 	"mosn.io/layotto/components/cryption"
 	aliyun_cryption "mosn.io/layotto/components/cryption/aliyun"
 	aws_cryption "mosn.io/layotto/components/cryption/aws"
+	tencentcloud_sms "mosn.io/layotto/components/sms/tencentcloud"
 
 	"mosn.io/layotto/pkg/grpc/lifecycle"
 
@@ -424,6 +427,10 @@ func NewRuntimeGrpcServer(data json.RawMessage, opts ...grpc.ServerOption) (mgrp
 			cryption.NewFactory("aliyun.kms", aliyun_cryption.NewCryption),
 			cryption.NewFactory("aws.kms", aws_cryption.NewCryption),
 		),
+		// Sms
+		runtime.WithSmsServiceFactory(
+			sms.NewFactory("tencentcloud.sms", tencentcloud_sms.NewSms),
+		),
 		// Lock
 		runtime.WithLockFactory(
 			runtime_lock.NewFactory("redis_cluster", func() lock.LockStore {
diff --git a/cmd/layotto_without_xds/main.go b/cmd/layotto_without_xds/main.go
index 4160daec..daa27782 100644
--- a/cmd/layotto_without_xds/main.go
+++ b/cmd/layotto_without_xds/main.go
@@ -23,9 +23,12 @@ import (
 	"strconv"
 	"time"
 
+	"mosn.io/layotto/components/sms"
+
 	"mosn.io/layotto/components/cryption"
 	aliyun_cryption "mosn.io/layotto/components/cryption/aliyun"
 	aws_cryption "mosn.io/layotto/components/cryption/aws"
+	tencentcloud_sms "mosn.io/layotto/components/sms/tencentcloud"
 
 	"mosn.io/layotto/pkg/grpc/lifecycle"
 
@@ -428,6 +431,10 @@ func NewRuntimeGrpcServer(data json.RawMessage, opts ...grpc.ServerOption) (mgrp
 			cryption.NewFactory("aliyun.kms", aliyun_cryption.NewCryption),
 			cryption.NewFactory("aws.kms", aws_cryption.NewCryption),
 		),
+		// Sms
+		runtime.WithSmsServiceFactory(
+			sms.NewFactory("tencentcloud.sms", tencentcloud_sms.NewSms),
+		),
 		// Sequencer
 		runtime.WithSequencerFactory(
 			runtime_sequencer.NewFactory("etcd", func() sequencer.Store {
diff --git a/components/go.mod b/components/go.mod
index 501dd5e4..4b4d1d25 100644
--- a/components/go.mod
+++ b/components/go.mod
@@ -34,6 +34,8 @@ require (
 	github.com/pkg/errors v0.9.1
 	github.com/qiniu/go-sdk/v7 v7.11.1
 	github.com/stretchr/testify v1.8.0
+	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.759
+	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.759
 	github.com/tencentyun/cos-go-sdk-v5 v0.7.33
 	github.com/valyala/fasthttp v1.40.0
 	go.beyondstorage.io/services/hdfs v0.3.0
diff --git a/components/go.sum b/components/go.sum
index c836b6ba..a829722d 100644
--- a/components/go.sum
+++ b/components/go.sum
@@ -1201,7 +1201,11 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69
 github.com/supplyon/gremcos v0.1.0/go.mod h1:ZnXsXGVbGCYDFU5GLPX9HZLWfD+ZWkiPo30KUjNoOtw=
 github.com/tebeka/strftime v0.1.3/go.mod h1:7wJm3dZlpr4l/oVK0t1HYIc4rMzQ2XJlOMIUJUJH6XQ=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.194/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.759 h1:flWgFybB3MYWFxwRO4yXbdiPT3SNwjSLuXCXsfs6kN4=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.759/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.194/go.mod h1:yrBKWhChnDqNz1xuXdSbWXG56XawEq0G5j1lg4VwBD4=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.759 h1:GrpwnCcbPYewCj/lvyAWEShTprW+5CVWrU+89UyjXcs=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.759/go.mod h1:PEv4IxnbGqYejMUrUxawrlKHQUiAvgEXHpYP4Rfa538=
 github.com/tencentyun/cos-go-sdk-v5 v0.7.33 h1:5jmJU7U/1nf/7ZPDkrUL8KlF1oDUzTHsdtLNY6x0hq4=
 github.com/tencentyun/cos-go-sdk-v5 v0.7.33/go.mod h1:4E4+bQ2gBVJcgEC9Cufwylio4mXOct2iu05WjgEBx1o=
 github.com/testcontainers/testcontainers-go v0.9.0/go.mod h1:b22BFXhRbg4PJmeMVWh6ftqjyZHgiIl3w274e9r3C2E=
diff --git a/components/sms/errors.go b/components/sms/errors.go
new file mode 100644
index 00000000..f7f22906
--- /dev/null
+++ b/components/sms/errors.go
@@ -0,0 +1,35 @@
+// Copyright 2021 Layotto Authors
+// 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 sms
+
+import (
+	"errors"
+	"fmt"
+)
+
+var (
+	ErrClientNotInit = errors.New("error: client not init")
+)
+
+func MissingMethodParam(method, param string) error {
+	return fmt.Errorf("error: sms `%s` method missing parameter `%s`", method, param)
+}
+
+func MissingInitParam(param string) error {
+	return MissingMethodParam("init", param)
+}
+
+func MissingSendSmsParam(param string) error {
+	return MissingMethodParam("sendSms", param)
+}
diff --git a/components/sms/meta.go b/components/sms/meta.go
new file mode 100644
index 00000000..27aeb6b7
--- /dev/null
+++ b/components/sms/meta.go
@@ -0,0 +1,31 @@
+// Copyright 2021 Layotto Authors
+// 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 sms
+
+// metadata required to init sms client
+const (
+	ClientKey    = "accessKeyID"
+	ClientSecret = "accessKeySecret"
+	Region       = "region"
+)
+
+// metadata required to tencentcloud sms request
+const (
+	SdkAppId = "SdkAppId"
+)
+
+// metadata contained in sms response
+const (
+	PhoneNumber = "PhoneNumber"
+)
diff --git a/components/sms/types_generated.go b/components/sms/registry.go
similarity index 82%
rename from components/sms/types_generated.go
rename to components/sms/registry.go
index 1eb37a0f..3ce75163 100644
--- a/components/sms/types_generated.go
+++ b/components/sms/registry.go
@@ -1,5 +1,3 @@
-// Code generated by github.com/layotto/protoc-gen-p6 .
-
 // Copyright 2021 Layotto Authors
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -16,23 +14,11 @@
 package sms
 
 import (
-	fmt "fmt"
-
-	info "mosn.io/layotto/components/pkg/info"
-	ref "mosn.io/layotto/components/ref"
-)
+	"fmt"
 
-const (
-	serviceName = "sms"
+	"mosn.io/layotto/components/pkg/info"
 )
 
-// Config is the component's configuration
-type Config struct {
-	ref.Config
-	Type     string            `json:"type"`
-	Metadata map[string]string `json:"metadata"`
-}
-
 type Registry interface {
 	Register(fs ...*Factory)
 	Create(compType string) (SmsService, error)
diff --git a/components/sms/interface_generated.go b/components/sms/sms.go
similarity index 91%
rename from components/sms/interface_generated.go
rename to components/sms/sms.go
index 3c943fe3..b02a9398 100644
--- a/components/sms/interface_generated.go
+++ b/components/sms/sms.go
@@ -1,5 +1,3 @@
-// Code generated by github.com/layotto/protoc-gen-p6 .
-
 // Copyright 2021 Layotto Authors
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -16,7 +14,11 @@
 package sms
 
 import (
-	context "context"
+	"context"
+)
+
+const (
+	serviceName = "sms"
 )
 
 type SmsService interface {
diff --git a/components/sms/tencentcloud/sms.go b/components/sms/tencentcloud/sms.go
new file mode 100644
index 00000000..5cbade16
--- /dev/null
+++ b/components/sms/tencentcloud/sms.go
@@ -0,0 +1,203 @@
+// Copyright 2021 Layotto Authors
+// 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 tencentcloud
+
+import (
+	"context"
+	"errors"
+	"strconv"
+
+	"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
+	"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
+	tcsms "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111"
+
+	"mosn.io/layotto/components/sms"
+)
+
+var (
+	ErrTemplateParams = errors.New("error: template parameters should be key-value pairs of the form [idx, param], with idx starting at 0 and ending at the length of parameters - 1")
+)
+
+// SmsClient defines the methods of the tencentcloud sms client.
+type SmsClient interface {
+	SendSmsWithContext(ctx context.Context, request *tcsms.SendSmsRequest) (response *tcsms.SendSmsResponse, err error)
+}
+
+// InitConfig is the information required to initialize the sms client
+type InitConfig struct {
+	SecretId  string
+	SecretKey string
+	Region    string
+}
+
+// NewInitConfig create init config, which will be used in NewSmsClient.
+// It checks metadata from sms.Config
+// The `accessKeyID` should not be empty.
+// The `accessKeySecret` should not be empty.
+// The `region` should not be empty.
+// Please make sure `region` value is available,
+// you can refer https://cloud.tencent.com/document/api/382/52071
+func NewInitConfig(config *sms.Config) (*InitConfig, error) {
+	meta := config.Metadata
+	secretId := meta[sms.ClientKey]
+	if secretId == "" {
+		return nil, sms.MissingInitParam(sms.ClientKey)
+	}
+	secretKey := meta[sms.ClientSecret]
+	if secretKey == "" {
+		return nil, sms.MissingInitParam(sms.ClientSecret)
+	}
+	region := meta[sms.Region]
+	if region == "" {
+		return nil, sms.MissingInitParam(sms.Region)
+	}
+
+	conf := &InitConfig{
+		SecretId:  secretId,
+		SecretKey: secretKey,
+		Region:    region,
+	}
+	return conf, nil
+}
+
+// NewSmsClient create tencentcloud sms client instance by InitConfig.
+func NewSmsClient(conf *InitConfig) (SmsClient, error) {
+	credential := common.NewCredential(conf.SecretId, conf.SecretKey)
+	cpf := profile.NewClientProfile()
+	return tcsms.NewClient(credential, conf.Region, cpf)
+}
+
+// NewSendSmsRequest create tencentcloud sms request struct by sms.SendSmsWithTemplateRequest.
+// The PhoneNumbers should not be empty.
+// The Template should not be empty.
+// In Template, the TemplateParams should be key-value pairs of the form [idx, param],
+// with idx starting at 0 and ending at the length of parameters - 1,
+// each [idx, param] key-value pair indicates that the value of the variable at the idx position is param.
+// Each parameter and its index must match the variable position of the template corresponding to TemplateId,
+// you can refer https://cloud.tencent.com/document/api/382/55981
+func NewSendSmsRequest(req *sms.SendSmsWithTemplateRequest) (*tcsms.SendSmsRequest, error) {
+	if len(req.PhoneNumbers) == 0 {
+		return nil, sms.MissingSendSmsParam("phone_numbers")
+	}
+	if req.Template == nil {
+		return nil, sms.MissingSendSmsParam("template")
+	}
+
+	n := len(req.Template.TemplateParams)
+	templateParams := make([]string, n)
+	for k, v := range req.Template.TemplateParams {
+		idx, err := strconv.Atoi(k)
+		if err != nil {
+			return nil, ErrTemplateParams
+		}
+		if idx < 0 || idx >= n {
+			return nil, ErrTemplateParams
+		}
+		templateParams[idx] = v
+	}
+
+	meta := req.Metadata
+	request := tcsms.NewSendSmsRequest()
+	// required fields
+	request.PhoneNumberSet = common.StringPtrs(req.PhoneNumbers)
+	request.SmsSdkAppId = common.StringPtr(meta[sms.SdkAppId])
+	request.TemplateId = common.StringPtr(req.Template.TemplateId)
+	// optional fields
+	request.SignName = common.StringPtr(req.SignName)
+	request.SenderId = common.StringPtr(req.SenderId)
+	request.TemplateParamSet = common.StringPtrs(templateParams)
+	return request, nil
+}
+
+// ConvertSmsResponse convert tcsms.SendSmsResponse to sms.SendSmsWithTemplateResponse
+func ConvertSmsResponse(resp *tcsms.SendSmsResponse) *sms.SendSmsWithTemplateResponse {
+	statusSet := resp.Response.SendStatusSet
+	results := make([]*sms.SendStatus, len(statusSet))
+	for i, s := range statusSet {
+		meta := map[string]string{sms.PhoneNumber: *s.PhoneNumber}
+		results[i] = &sms.SendStatus{
+			Code:     *s.Code,
+			Message:  *s.Message,
+			Metadata: meta,
+		}
+	}
+	smsResp := &sms.SendSmsWithTemplateResponse{
+		RequestId: *resp.Response.RequestId,
+		Results:   results,
+	}
+	return smsResp
+}
+
+// Sms implemented sms.Sms, is used to send request to tencentcloud sms
+type Sms struct {
+	client SmsClient
+}
+
+// NewSms create empty sms client for tencentcloud
+func NewSms() sms.SmsService {
+	return &Sms{}
+}
+
+// Init used to init tencentcloud sms client
+// It checks metadata from sms.Config
+// The `accessKeyID` should not be empty.
+// The `accessKeySecret` should not be empty.
+// The `region` should not be empty.
+// Please make sure `region` value is available,
+// you can refer https://cloud.tencent.com/document/api/382/52071
+func (s *Sms) Init(ctx context.Context, config *sms.Config) error {
+	conf, err := NewInitConfig(config)
+	if err != nil {
+		return err
+	}
+	client, err := NewSmsClient(conf)
+	if err != nil {
+		return err
+	}
+	s.client = client
+	return nil
+}
+
+// SendSmsWithTemplate used to send sms with template to tencentcloud sms
+// Before calling this method, you should make sure that the Init method is called.
+// The PhoneNumbers should not be empty.
+// The Template should not be empty.
+// In Template, the TemplateParams should be key-value pairs of the form [idx, param],
+// with idx starting at 0 and ending at the length of parameters - 1,
+// each [idx, param] key-value pair indicates that the value of the variable at the idx position is param.
+// Each parameter and its index must match the variable position of the template corresponding to TemplateId,
+// you can refer https://cloud.tencent.com/document/api/382/55981
+func (s *Sms) SendSmsWithTemplate(ctx context.Context, request *sms.SendSmsWithTemplateRequest) (*sms.SendSmsWithTemplateResponse, error) {
+	client, err := s.getClient()
+	if err != nil {
+		return nil, err
+	}
+	smsRequest, err := NewSendSmsRequest(request)
+	if err != nil {
+		return nil, err
+	}
+	resp, err := client.SendSmsWithContext(ctx, smsRequest)
+	if err != nil {
+		return nil, err
+	}
+	smsResp := ConvertSmsResponse(resp)
+	return smsResp, nil
+}
+
+func (s *Sms) getClient() (SmsClient, error) {
+	if s.client == nil {
+		return nil, sms.ErrClientNotInit
+	}
+	return s.client, nil
+}
diff --git a/components/sms/struct_generated.go b/components/sms/types.go
similarity index 77%
rename from components/sms/struct_generated.go
rename to components/sms/types.go
index b07feb95..e8bbb537 100644
--- a/components/sms/struct_generated.go
+++ b/components/sms/types.go
@@ -1,5 +1,3 @@
-// Code generated by github.com/layotto/protoc-gen-p6 .
-
 // Copyright 2021 Layotto Authors
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -15,6 +13,17 @@
 
 package sms
 
+import (
+	ref "mosn.io/layotto/components/ref"
+)
+
+// Config is the component's configuration
+type Config struct {
+	ref.Config
+	Type     string            `json:"type"`
+	Metadata map[string]string `json:"metadata"`
+}
+
 // SendSmsRequest is the request of the `SendSms` method.
 type SendSmsWithTemplateRequest struct {
 	// Required. The saas service name
@@ -45,10 +54,18 @@ type Template struct {
 type SendSmsWithTemplateResponse struct {
 	// The unique requestId.
 	RequestId string `json:"request_id,omitempty"`
+	// The status set of SMS
+	Results []*SendStatus `json:"results,omitempty"`
+}
+
+// Status contains more information about the response
+type SendStatus struct {
 	// "OK" represents success.
 	Code string `json:"code,omitempty"`
 	// The error message.
 	Message string `json:"message,omitempty"`
-	// The metadata returned from SMS service.
+	// The send status metadata returned from SMS service.
+	// Includes `PhoneNumber`.
+	// `PhoneNumber`, is the phone number SMS send to. Supported by tencentcloud.
 	Metadata map[string]string `json:"metadata,omitempty"`
 }
diff --git a/configs/config_sms_tencentcloud.json b/configs/config_sms_tencentcloud.json
new file mode 100644
index 00000000..f07246bf
--- /dev/null
+++ b/configs/config_sms_tencentcloud.json
@@ -0,0 +1,82 @@
+{
+  "servers": [
+    {
+      "default_log_path": "stdout",
+      "default_log_level": "DEBUG",
+      "listeners": [
+        {
+          "name": "grpc",
+          "address": "127.0.0.1:34904",
+          "bind_port": true,
+          "filter_chains": [
+            {
+              "filters": [
+                {
+                  "type": "grpc",
+                  "config": {
+                    "server_name": "runtime",
+                    "grpc_config": {
+                      "sms": {
+                        "sms_demo": {
+                          "type": "tencentcloud.sms",
+                          "metadata": {
+                            "accessKeyID": "access_key",
+                            "accessKeySecret": "access_secret",
+                            "region": "region"
+                          }
+                        }
+                      }
+                    }
+                  }
+                }
+              ]
+            }
+          ]
+        }
+      ]
+    }
+  ],
+  "dynamic_resources": {
+    "lds_config": {
+      "ads": {},
+      "initial_fetch_timeout": "0s",
+      "resource_api_version": "V3"
+    },
+    "cds_config": {
+      "ads": {},
+      "initial_fetch_timeout": "0s",
+      "resource_api_version": "V3"
+    },
+    "ads_config": {
+      "api_type": "GRPC",
+      "set_node_on_first_message_only": true,
+      "transport_api_version": "V3",
+      "grpc_services": [{
+        "envoy_grpc": {
+          "cluster_name": "xds-grpc"
+        }
+      }]
+    }
+  },
+  "static_resources": {
+    "clusters": [{
+      "name": "xds-grpc",
+      "type": "STATIC",
+      "connect_timeout": "1s",
+      "lb_policy": "ROUND_ROBIN",
+      "load_assignment": {
+        "cluster_name": "xds-grpc",
+        "endpoints": [{
+          "lb_endpoints": [{
+            "endpoint": {
+              "address": {
+                "socket_address": {"address": "127.0.0.1", "port_value": 30681}
+              }
+            }
+          }
+          ]
+        }]
+      }
+    }]
+  }
+}
diff --git a/demo/sms/tencentcloud/client.go b/demo/sms/tencentcloud/client.go
new file mode 100644
index 00000000..4c9921ce
--- /dev/null
+++ b/demo/sms/tencentcloud/client.go
@@ -0,0 +1,60 @@
+// Copyright 2021 Layotto Authors
+// 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 main
+
+import (
+	"context"
+	"fmt"
+
+	"google.golang.org/grpc"
+
+	smsv1 "mosn.io/layotto/spec/proto/extension/v1/sms"
+)
+
+var (
+	componentName = "sms_demo"
+)
+
+func main() {
+	// Dial to the gRPC server
+	conn, err := grpc.Dial("127.0.0.1:34904", grpc.WithInsecure())
+	if err != nil {
+		fmt.Printf("conn build failed,err: %+v", err)
+		panic(err)
+	}
+	defer conn.Close()
+
+	// Create a new client
+	client := smsv1.NewSmsServiceClient(conn)
+
+	// Make a request to send sms
+	request := &smsv1.SendSmsWithTemplateRequest{
+		ComponentName: componentName,
+		PhoneNumbers:  []string{"+8610001000100"},
+		Template: &smsv1.Template{
+			TemplateId:     "10000",
+			TemplateParams: map[string]string{},
+		},
+		SignName: "sign_name",
+		Metadata: map[string]string{"SdkAppId": "app_id"},
+	}
+	response, err := client.SendSmsWithTemplate(context.Background(), request)
+	if err != nil {
+		fmt.Printf("send sms failed: %+v", err)
+		panic(err)
+	}
+
+	// Print the result of the SendSmsWithTemplate response
+	fmt.Println(response)
+}
diff --git a/go.mod b/go.mod
index 6ec0843f..82781dfb 100644
--- a/go.mod
+++ b/go.mod
@@ -252,6 +252,8 @@ require (
 	github.com/streadway/amqp v1.0.0 // indirect
 	github.com/stretchr/objx v0.4.0 // indirect
 	github.com/subosito/gotenv v1.2.0 // indirect
+	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.759 // indirect
+	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.759 // indirect
 	github.com/tencentyun/cos-go-sdk-v5 v0.7.33 // indirect
 	github.com/tjfoc/gmsm v1.3.2 // indirect
 	github.com/tklauser/go-sysconf v0.3.6 // indirect
diff --git a/go.sum b/go.sum
index b4226d84..2a0889b5 100644
--- a/go.sum
+++ b/go.sum
@@ -1285,7 +1285,11 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69
 github.com/supplyon/gremcos v0.1.0/go.mod h1:ZnXsXGVbGCYDFU5GLPX9HZLWfD+ZWkiPo30KUjNoOtw=
 github.com/tebeka/strftime v0.1.3/go.mod h1:7wJm3dZlpr4l/oVK0t1HYIc4rMzQ2XJlOMIUJUJH6XQ=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.194/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.759 h1:flWgFybB3MYWFxwRO4yXbdiPT3SNwjSLuXCXsfs6kN4=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.759/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.194/go.mod h1:yrBKWhChnDqNz1xuXdSbWXG56XawEq0G5j1lg4VwBD4=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.759 h1:GrpwnCcbPYewCj/lvyAWEShTprW+5CVWrU+89UyjXcs=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.759/go.mod h1:PEv4IxnbGqYejMUrUxawrlKHQUiAvgEXHpYP4Rfa538=
 github.com/tencentyun/cos-go-sdk-v5 v0.7.33 h1:5jmJU7U/1nf/7ZPDkrUL8KlF1oDUzTHsdtLNY6x0hq4=
 github.com/tencentyun/cos-go-sdk-v5 v0.7.33/go.mod h1:4E4+bQ2gBVJcgEC9Cufwylio4mXOct2iu05WjgEBx1o=
 github.com/testcontainers/testcontainers-go v0.9.0/go.mod h1:b22BFXhRbg4PJmeMVWh6ftqjyZHgiIl3w274e9r3C2E=


Test patch:
diff --git a/components/sms/tencentcloud/export_test.go b/components/sms/tencentcloud/export_test.go
new file mode 100644
index 00000000..572f4b62
--- /dev/null
+++ b/components/sms/tencentcloud/export_test.go
@@ -0,0 +1,20 @@
+// Copyright 2021 Layotto Authors
+// 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 tencentcloud
+
+import "mosn.io/layotto/components/sms"
+
+func NewSmsWithClient(client SmsClient) sms.SmsService {
+	return &Sms{client: client}
+}
diff --git a/components/sms/tencentcloud/sms_test.go b/components/sms/tencentcloud/sms_test.go
new file mode 100644
index 00000000..dd9df23c
--- /dev/null
+++ b/components/sms/tencentcloud/sms_test.go
@@ -0,0 +1,183 @@
+// Copyright 2021 Layotto Authors
+// 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 tencentcloud_test
+
+import (
+	"context"
+	"errors"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
+	tcsms "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111"
+
+	"mosn.io/layotto/components/sms"
+	"mosn.io/layotto/components/sms/tencentcloud"
+)
+
+type MockSmsClient struct{}
+
+// MockSmsClient.SendSmsWithContext returns error when the TemplateId equals `-1`,
+// else returns empty response.
+func (*MockSmsClient) SendSmsWithContext(ctx context.Context, request *tcsms.SendSmsRequest) (*tcsms.SendSmsResponse, error) {
+	if *request.TemplateId == "-1" {
+		return nil, errors.New("need error")
+	}
+	resp := tcsms.NewSendSmsResponse()
+	resp.Response = &tcsms.SendSmsResponseParams{
+		SendStatusSet: []*tcsms.SendStatus{
+			{
+				Code:        common.StringPtr("code"),
+				Message:     common.StringPtr("message"),
+				PhoneNumber: common.StringPtr("phoneNumber"),
+			},
+		},
+		RequestId: common.StringPtr("requestId"),
+	}
+	return resp, nil
+}
+
+var (
+	ctx     = context.Background()
+	mockSms = tencentcloud.NewSmsWithClient(&MockSmsClient{})
+)
+
+var (
+	confSuccess = &sms.Config{
+		Metadata: map[string]string{
+			sms.ClientKey:    "ck",
+			sms.ClientSecret: "cs",
+			sms.Region:       "region",
+		},
+	}
+	confNoClientKey = &sms.Config{
+		Metadata: map[string]string{
+			sms.ClientSecret: "cs",
+			sms.Region:       "region",
+		},
+	}
+	confNoClientSecret = &sms.Config{
+		Metadata: map[string]string{
+			sms.ClientKey: "ck",
+			sms.Region:    "region",
+		},
+	}
+	confNoRegion = &sms.Config{
+		Metadata: map[string]string{
+			sms.ClientKey:    "ck",
+			sms.ClientSecret: "cs",
+		},
+	}
+)
+
+var (
+	requestSuccess = &sms.SendSmsWithTemplateRequest{
+		PhoneNumbers: []string{"+0010001000100"},
+		Template:     &sms.Template{TemplateParams: map[string]string{"0": "param"}},
+	}
+	requestNoPhoneNumbers = &sms.SendSmsWithTemplateRequest{
+		Template: &sms.Template{TemplateParams: map[string]string{"0": "param"}},
+	}
+	requestNoTemplate = &sms.SendSmsWithTemplateRequest{
+		PhoneNumbers: []string{"+0010001000100"},
+	}
+	requestWithWrongTemplate1 = &sms.SendSmsWithTemplateRequest{
+		PhoneNumbers: []string{"+0010001000100"},
+		Template:     &sms.Template{TemplateParams: map[string]string{"idx": "param"}},
+	}
+	requestWithWrongTemplate2 = &sms.SendSmsWithTemplateRequest{
+		PhoneNumbers: []string{"+0010001000100"},
+		Template:     &sms.Template{TemplateParams: map[string]string{"-1": "param"}},
+	}
+	requestFailed = &sms.SendSmsWithTemplateRequest{
+		PhoneNumbers: []string{"+0010001000100"},
+		Template: &sms.Template{
+			TemplateId:     "-1",
+			TemplateParams: map[string]string{"0": "param"},
+		},
+	}
+)
+
+func testInit(t *testing.T) {
+	svc := tencentcloud.NewSms()
+	err := svc.Init(ctx, confSuccess)
+	assert.NoError(t, err)
+}
+
+func testInitNoClientKey(t *testing.T) {
+	svc := tencentcloud.NewSms()
+	err := svc.Init(ctx, confNoClientKey)
+	assert.Error(t, err)
+}
+
+func testInitNoClientSecret(t *testing.T) {
+	svc := tencentcloud.NewSms()
+	err := svc.Init(ctx, confNoClientSecret)
+	assert.Error(t, err)
+}
+
+func testInitNoRegion(t *testing.T) {
+	svc := tencentcloud.NewSms()
+	err := svc.Init(ctx, confNoRegion)
+	assert.Error(t, err)
+}
+
+func TestSms_Init(t *testing.T) {
+	t.Run("TestInit", testInit)
+	t.Run("TestInitNoClientKey", testInitNoClientKey)
+	t.Run("TestInitNoClientSecret", testInitNoClientSecret)
+	t.Run("TestInitNoRegion", testInitNoRegion)
+}
+
+func testSendSms(t *testing.T) {
+	_, err := mockSms.SendSmsWithTemplate(ctx, requestSuccess)
+	assert.NoError(t, err)
+}
+
+func testSendSmsNotInit(t *testing.T) {
+	svc := tencentcloud.NewSms()
+	_, err := svc.SendSmsWithTemplate(ctx, requestSuccess)
+	assert.Error(t, err)
+}
+
+func testSendSmsNoPhoneNumbers(t *testing.T) {
+	_, err := mockSms.SendSmsWithTemplate(ctx, requestNoPhoneNumbers)
+	assert.Error(t, err)
+}
+
+func testSendSmsNoTemplate(t *testing.T) {
+	_, err := mockSms.SendSmsWithTemplate(ctx, requestNoTemplate)
+	assert.Error(t, err)
+}
+
+func testSendSmsWithWrongTemplate(t *testing.T) {
+	_, err := mockSms.SendSmsWithTemplate(ctx, requestWithWrongTemplate1)
+	assert.Error(t, err)
+	_, err = mockSms.SendSmsWithTemplate(ctx, requestWithWrongTemplate2)
+	assert.Error(t, err)
+}
+
+func testSendSmsFailed(t *testing.T) {
+	_, err := mockSms.SendSmsWithTemplate(ctx, requestFailed)
+	assert.Error(t, err)
+}
+
+func TestSms_SendSmsWithTemplate(t *testing.T) {
+	t.Run("TestSendSms", testSendSms)
+	t.Run("TestSendSmsNotInit", testSendSmsNotInit)
+	t.Run("TestSendSmsNoPhoneNumbers", testSendSmsNoPhoneNumbers)
+	t.Run("TestSendSmsNoTemplate", testSendSmsNoTemplate)
+	t.Run("TestSendSmsWithWrongTemplate", testSendSmsWithWrongTemplate)
+	t.Run("TestSendSmsFailed", testSendSmsFailed)
+}