You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ju...@apache.org on 2020/07/12 04:55:22 UTC

[incubator-apisix-dashboard] branch master updated: add transaction for ssl and consumer (#308)

This is an automated email from the ASF dual-hosted git repository.

juzhiyuan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-apisix-dashboard.git


The following commit(s) were added to refs/heads/master by this push:
     new 08e65f4  add transaction for ssl and consumer (#308)
08e65f4 is described below

commit 08e65f48983608dbab91947775e88ad668eeff90
Author: nic-chen <33...@users.noreply.github.com>
AuthorDate: Sun Jul 12 12:55:11 2020 +0800

    add transaction for ssl and consumer (#308)
---
 api/errno/error.go       | 113 ++++++++++++----------
 api/script/db/schema.sql |   4 +-
 api/service/consumer.go  | 130 ++++++++++++++++++++-----
 api/service/ssl.go       | 245 +++++++++++++++++++++++++++++++++++++++++------
 api/service/ssl_test.go  |  86 ++++++++++++++++-
 5 files changed, 463 insertions(+), 115 deletions(-)

diff --git a/api/errno/error.go b/api/errno/error.go
index 24bb5a3..08b5adb 100644
--- a/api/errno/error.go
+++ b/api/errno/error.go
@@ -22,65 +22,74 @@ import (
 )
 
 type Message struct {
-	Code string
-	Msg  string
+	Code   string
+	Msg    string
+	Status int `json:"-"`
 }
 
 var (
 	//AA 01 api-manager-api
-	SystemSuccess   = Message{"010000", "success"}
-	SystemError     = Message{"010001", "system error"}
-	BadRequestError = Message{Code: "010002", Msg: "Request format error"}
-	NotFoundError   = Message{Code: "010003", Msg: "No resources found"}
-	InvalidParam    = Message{"010004", "Request parameter error"}
-	DBWriteError    = Message{"010005", "Database save failed"}
-	DBReadError     = Message{"010006", "Database query failed"}
-	DBDeleteError   = Message{"010007", "Database delete failed"}
-	RecordNotExist  = Message{"010009", "Record does not exist"}
-
-	//BB 01 config module
-	ConfEnvError      = Message{"010101", "Environment variable not found: %s"}
-	ConfFilePathError = Message{"010102", "Error loading configuration file: %s"}
-
-	// BB 02 route module
-	RouteRequestError       = Message{"010201", "Route request parameters are abnormal: %s"}
-	ApisixRouteCreateError  = Message{"010202", "Failed to create APISIX route: %s"}
-	DBRouteCreateError      = Message{"010203", "Route storage failure: %s"}
-	ApisixRouteUpdateError  = Message{"010204", "Update APISIX routing failed: %s"}
-	ApisixRouteDeleteError  = Message{"010205", "Failed to remove APISIX route: %s"}
-	DBRouteUpdateError      = Message{"010206", "Route update failed: %s"}
-	DBRouteDeleteError      = Message{"010207", "Route remove failed: %s"}
-	DBRouteReduplicateError = Message{"010208", "Route name is reduplicate : %s"}
-
-	// 03 plugin module
-	ApisixPluginListError   = Message{"010301", "List APISIX plugins  failed: %s"}
-	ApisixPluginSchemaError = Message{"010301", "Find APISIX plugin schema failed: %s"}
-
-	// 04 ssl模块
-	SslParseError        = Message{"010401", "Certificate resolution failed: %s"}
-	ApisixSslCreateError = Message{"010402", "Create APISIX SSL failed"}
-	ApisixSslUpdateError = Message{"010403", "Update APISIX SSL failed"}
-	ApisixSslDeleteError = Message{"010404", "Delete APISIX SSL failed"}
+	//BB 00 system
+	SystemSuccess      = Message{"010000", "success", 200}
+	SystemError        = Message{"010001", "system error", 500}
+	BadRequestError    = Message{"010002", "Request format error", 400}
+	NotFoundError      = Message{"010003", "No resources found", 404}
+	InvalidParam       = Message{"010004", "Request parameter error", 400}
+	DBWriteError       = Message{"010005", "Database save failed", 500}
+	DBReadError        = Message{"010006", "Database query failed", 500}
+	DBDeleteError      = Message{"010007", "Database delete failed", 500}
+	RecordNotExist     = Message{"010009", "Record does not exist", 404}
+	InvalidParamDetail = Message{"010010", "Invalid request parameter: %s", 400}
+	AdminApiSaveError  = Message{"010011", "Data save failed", 500}
+	SchemaCheckFailed  = Message{"010012", "%s", 400}
+
+	//BB 01 configuration
+	ConfEnvError      = Message{"010101", "Environment variable not found: %s", 500}
+	ConfFilePathError = Message{"010102", "Error loading configuration file: %s", 500}
+
+	// BB 02 route
+	RouteRequestError       = Message{"010201", "Route request parameters are abnormal: %s", 400}
+	ApisixRouteCreateError  = Message{"010202", "Failed to create APISIX route: %s", 500}
+	DBRouteCreateError      = Message{"010203", "Route storage failure: %s", 500}
+	ApisixRouteUpdateError  = Message{"010204", "Update APISIX routing failed: %s", 500}
+	ApisixRouteDeleteError  = Message{"010205", "Failed to delete APISIX route: %s", 500}
+	DBRouteUpdateError      = Message{"010206", "Route update failed: %s", 500}
+	DBRouteDeleteError      = Message{"010207", "Route deletion failed: %s", 500}
+	DBRouteReduplicateError = Message{"010208", "Route name is reduplicate : %s", 400}
+
+	// 03 plugins
+	ApisixPluginListError   = Message{"010301", "find APISIX plugin list failed: %s", 500}
+	ApisixPluginSchemaError = Message{"010301", "find APISIX plugin schema failed: %s", 500}
+
+	// 04 ssl
+	SslParseError        = Message{"010401", "Certificate resolution failed: %s", 400}
+	ApisixSslCreateError = Message{"010402", "Failed to create APISIX SSL", 500}
+	ApisixSslUpdateError = Message{"010403", "Failed to update APISIX SSL", 500}
+	ApisixSslDeleteError = Message{"010404", "Failed to delete APISIX SSL", 500}
+	SslForSniNotExists   = Message{"010407", "Ssl for sni not exists:%s", 400}
+	DuplicateSslCert     = Message{"010408", "Duplicate ssl cert", 400}
 
 	// 06 upstream
-	UpstreamRequestError       = Message{"010601", "upstream request parameters are abnormal: %s"}
-	UpstreamTransError         = Message{"010602", "upstream parameter conversion is abnormal: %s"}
-	DBUpstreamError            = Message{"010603", "upstream storage failure: %s"}
-	ApisixUpstreamCreateError  = Message{"010604", "apisix upstream create failure: %s"}
-	ApisixUpstreamUpdateError  = Message{"010605", "apisix upstream update failure: %s"}
-	ApisixUpstreamDeleteError  = Message{"010606", "apisix upstream delete failure: %s"}
-	DBUpstreamDeleteError      = Message{"010607", "upstream delete failure: %s"}
-	DBUpstreamReduplicateError = Message{"010608", "Upstream name is reduplicate : %s"}
-
-	ApisixConsumerCreateError = Message{"010702", "Create APISIX Consumer failed"}
-	ApisixConsumerUpdateError = Message{"010703", "Update APISIX Consumer failed"}
-	ApisixConsumerDeleteError = Message{"010704", "Delete APISIX Consumer failed"}
-	DuplicateUserName         = Message{"010705", "Duplicate username"}
+	UpstreamRequestError       = Message{"010601", "upstream request parameters exception: %s", 400}
+	UpstreamTransError         = Message{"010602", "Abnormal upstream parameter conversion: %s", 400}
+	DBUpstreamError            = Message{"010603", "upstream storage failure: %s", 500}
+	ApisixUpstreamCreateError  = Message{"010604", "apisix upstream create failed: %s", 500}
+	ApisixUpstreamUpdateError  = Message{"010605", "apisix upstream update failed: %s", 500}
+	ApisixUpstreamDeleteError  = Message{"010606", "apisix upstream delete failed: %s", 500}
+	DBUpstreamDeleteError      = Message{"010607", "upstream storage delete failed: %s", 500}
+	DBUpstreamReduplicateError = Message{"010608", "Upstream name is reduplicate : %s", 500}
+
+	// 07 consumer
+	ApisixConsumerCreateError = Message{"010702", "APISIX Consumer create failed", 500}
+	ApisixConsumerUpdateError = Message{"010703", "APISIX Consumer update failed", 500}
+	ApisixConsumerDeleteError = Message{"010704", "APISIX Consumer delete failed", 500}
+	DuplicateUserName         = Message{"010705", "Duplicate consumer username", 400}
 )
 
 type ManagerError struct {
 	TraceId string
 	Code    string
+	Status  int
 	Msg     string
 	Data    interface{}
 	Detail  string
@@ -96,11 +105,11 @@ func (e *ManagerError) ErrorDetail() string {
 }
 
 func FromMessage(m Message, args ...interface{}) *ManagerError {
-	return &ManagerError{TraceId: "", Code: m.Code, Msg: fmt.Sprintf(m.Msg, args...)}
+	return &ManagerError{TraceId: "", Code: m.Code, Status: m.Status, Msg: fmt.Sprintf(m.Msg, args...)}
 }
 
 func New(m Message, args ...interface{}) *ManagerError {
-	return &ManagerError{TraceId: "", Code: m.Code, Msg: m.Msg, Detail: fmt.Sprintf("%s", args...)}
+	return &ManagerError{TraceId: "", Code: m.Code, Msg: m.Msg, Status: m.Status, Detail: fmt.Sprintf("%s", args...)}
 }
 
 func (e *ManagerError) Response() map[string]interface{} {
@@ -139,9 +148,9 @@ func Succeed() map[string]interface{} {
 
 type HttpError struct {
 	Code int
-	Msg  string
+	Msg  Message
 }
 
 func (e *HttpError) Error() string {
-	return e.Msg
+	return e.Msg.Msg
 }
diff --git a/api/script/db/schema.sql b/api/script/db/schema.sql
index 83c4bdd..95d4e60 100644
--- a/api/script/db/schema.sql
+++ b/api/script/db/schema.sql
@@ -28,7 +28,9 @@ CREATE TABLE `ssls` (
   `status` tinyint(1) unsigned NOT NULL DEFAULT '1',
   `create_time` bigint(20) unsigned NOT NULL,
   `update_time` bigint(20) unsigned NOT NULL,
-  PRIMARY KEY (`id`)
+  `public_key_hash` varchar(64) NOT NULL DEFAULT '',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uni_public_key_hash` (`public_key_hash`)
 ) DEFAULT CHARSET=utf8;
 
 -- upstream
diff --git a/api/service/consumer.go b/api/service/consumer.go
index a29dde6..98dee45 100644
--- a/api/service/consumer.go
+++ b/api/service/consumer.go
@@ -102,8 +102,8 @@ func ConsumerList(page, size int, search string) (int, []ConsumerDto, error) {
 	db := conf.DB().Table("consumers")
 
 	if search != "" {
-		db = db.Where("name like ? ", "%"+search+"%").
-			Or("description like ? ", "%"+search+"%")
+		db = db.Where("username like ? ", "%"+search+"%").
+			Or("`desc` like ? ", "%"+search+"%")
 	}
 
 	if err := db.Order("create_time desc").Offset((page - 1) * size).Limit(size).Find(&consumerList).Error; err != nil {
@@ -128,6 +128,10 @@ func ConsumerList(page, size int, search string) (int, []ConsumerDto, error) {
 }
 
 func ConsumerItem(id string) (*ConsumerDto, error) {
+	if id == "" {
+		return nil, errno.New(errno.InvalidParam)
+	}
+
 	consumer := &Consumer{}
 	if err := conf.DB().Table("consumers").Where("id = ?", id).First(consumer).Error; err != nil {
 		e := errno.New(errno.DBReadError, err.Error())
@@ -144,6 +148,14 @@ func ConsumerCreate(param interface{}, id string) error {
 	req := &ConsumerDto{}
 	req.Parse(param)
 
+	if req.Username == "" {
+		return errno.New(errno.InvalidParamDetail, "username is required")
+	}
+
+	if len(req.Desc) > 200 {
+		return errno.New(errno.InvalidParamDetail, "description is too long")
+	}
+
 	exists := Consumer{}
 	conf.DB().Table("consumers").Where("username = ?", req.Username).First(&exists)
 	if exists != (Consumer{}) {
@@ -151,10 +163,29 @@ func ConsumerCreate(param interface{}, id string) error {
 		return e
 	}
 
+	// trans
+	tx := conf.DB().Begin()
+	defer func() {
+		if r := recover(); r != nil {
+			tx.Rollback()
+		}
+	}()
+
+	consumer := &Consumer{}
+	consumer.Transfer(req)
+
+	// update mysql
+	consumer.ID = uuid.FromStringOrNil(id)
+	if err := tx.Create(consumer).Error; err != nil {
+		tx.Rollback()
+		return errno.New(errno.DBWriteError, err.Error())
+	}
+
 	apisixConsumer := &ApisixConsumer{}
 	apisixConsumer.Transfer(req)
 
 	if _, err := apisixConsumer.PutConsumerToApisix(req.Username); err != nil {
+		tx.Rollback()
 		if _, ok := err.(*errno.HttpError); ok {
 			return err
 		}
@@ -162,33 +193,56 @@ func ConsumerCreate(param interface{}, id string) error {
 		return e
 	}
 
-	consumer := &Consumer{}
-	consumer.Transfer(req)
-
-	// update mysql
-	consumer.ID = uuid.FromStringOrNil(id)
-	if err := conf.DB().Create(consumer).Error; err != nil {
-		return errno.New(errno.DBWriteError, err.Error())
-	}
+	tx.Commit()
 
 	return nil
 }
 
 func ConsumerUpdate(param interface{}, id string) error {
+	if id == "" {
+		return errno.New(errno.InvalidParam)
+	}
+
 	req := &ConsumerDto{}
 	req.Parse(param)
+	if req == nil {
+		return errno.New(errno.InvalidParam)
+	}
 
-	exists := Consumer{}
-	conf.DB().Table("consumers").Where("username = ?", req.Username).First(&exists)
-	if exists != (Consumer{}) && exists.ID != req.ID {
-		e := errno.New(errno.DuplicateUserName)
-		return e
+	req.ID = uuid.FromStringOrNil(id)
+	if req.Username != "" {
+		exists := Consumer{}
+		conf.DB().Table("consumers").Where("username = ?", req.Username).First(&exists)
+		if exists != (Consumer{}) && exists.ID != req.ID {
+			e := errno.New(errno.DuplicateUserName)
+			return e
+		}
+	}
+	// trans
+	tx := conf.DB().Begin()
+	defer func() {
+		if r := recover(); r != nil {
+			tx.Rollback()
+		}
+	}()
+
+	// update mysql
+	consumer := &Consumer{}
+	consumer.Transfer(req)
+	data := Consumer{Desc: consumer.Desc, Plugins: consumer.Plugins}
+	if req.Username != "" {
+		data.Username = req.Username
+	}
+	if err := tx.Model(&consumer).Updates(data).Error; err != nil {
+		tx.Rollback()
+		return errno.New(errno.DBWriteError, err.Error())
 	}
 
 	apisixConsumer := &ApisixConsumer{}
 	apisixConsumer.Transfer(req)
 
 	if _, err := apisixConsumer.PutConsumerToApisix(req.Username); err != nil {
+		tx.Rollback()
 		if _, ok := err.(*errno.HttpError); ok {
 			return err
 		}
@@ -196,18 +250,15 @@ func ConsumerUpdate(param interface{}, id string) error {
 		return e
 	}
 
-	// update mysql
-	consumer := &Consumer{}
-	consumer.Transfer(req)
-	consumer.ID = uuid.FromStringOrNil(id)
-	if err := conf.DB().Model(&consumer).Updates(consumer).Error; err != nil {
-		return errno.New(errno.DBWriteError, err.Error())
-	}
+	tx.Commit()
 
 	return nil
 }
 
 func ConsumerDelete(id string) error {
+	if id == "" {
+		return errno.New(errno.InvalidParam)
+	}
 	//
 	consumer := &Consumer{}
 	if err := conf.DB().Table("consumers").Where("id = ?", id).First(consumer).Error; err != nil {
@@ -215,17 +266,31 @@ func ConsumerDelete(id string) error {
 		return e
 	}
 
+	// trans
+	tx := conf.DB().Begin()
+	defer func() {
+		if r := recover(); r != nil {
+			tx.Rollback()
+		}
+	}()
+
+	// delete from mysql
+	if err := conf.DB().Delete(consumer).Error; err != nil {
+		tx.Rollback()
+		return errno.New(errno.DBDeleteError, err.Error())
+	}
+
+	//delete from apisix
 	if _, err := consumer.DeleteConsumerFromApisix(); err != nil {
+		tx.Rollback()
 		if _, ok := err.(*errno.HttpError); ok {
 			return err
 		}
 		e := errno.New(errno.ApisixConsumerDeleteError, err.Error())
 		return e
 	}
-	// delete from mysql
-	if err := conf.DB().Delete(consumer).Error; err != nil {
-		return errno.New(errno.DBDeleteError, err.Error())
-	}
+
+	tx.Commit()
 
 	return nil
 }
@@ -269,3 +334,16 @@ func (req *Consumer) DeleteConsumerFromApisix() (*ApisixConsumerResponse, error)
 		}
 	}
 }
+
+func GetConsumerByUserName(name string) (*Consumer, error) {
+	if name == "" {
+		return nil, errno.New(errno.InvalidParam)
+	}
+	consumer := &Consumer{}
+	if err := conf.DB().Table("consumers").Where("username = ?", name).First(consumer).Error; err != nil {
+		e := errno.New(errno.NotFoundError, err.Error())
+		return nil, e
+	}
+
+	return consumer, nil
+}
diff --git a/api/service/ssl.go b/api/service/ssl.go
index 08dea43..2fbfbfd 100644
--- a/api/service/ssl.go
+++ b/api/service/ssl.go
@@ -17,12 +17,15 @@
 package service
 
 import (
+	"crypto/md5"
 	"crypto/tls"
 	"crypto/x509"
 	"encoding/json"
 	"encoding/pem"
 	"errors"
 	"fmt"
+	"regexp"
+	"strings"
 
 	"github.com/apisix/manager-api/conf"
 	"github.com/apisix/manager-api/errno"
@@ -37,6 +40,7 @@ type Ssl struct {
 	Snis          string `json:"snis"`
 	Status        uint64 `json:"status"`
 	PublicKey     string `json:"public_key,omitempty"`
+	PublicKeyHash string `json:"public_key_hash,omitempty"`
 }
 
 type SslDto struct {
@@ -69,8 +73,9 @@ type SslNode struct {
 
 func (req *SslRequest) Parse(body interface{}) {
 	if err := json.Unmarshal(body.([]byte), req); err != nil {
+		logger.Info("req:")
+		logger.Info(req)
 		req = nil
-		logger.Error(errno.FromMessage(errno.RouteRequestError, err.Error()).Msg)
 	}
 }
 
@@ -91,7 +96,7 @@ func (sslDto *SslDto) Parse(ssl *Ssl) error {
 	return nil
 }
 
-func SslList(page, size, status, expireStart, expireEnd int, sni string) (int, []SslDto, error) {
+func SslList(page, size, status, expireStart, expireEnd int, sni, sortType string) (int, []SslDto, error) {
 	var count int
 	sslList := []Ssl{}
 	db := conf.DB().Table("ssls")
@@ -109,7 +114,12 @@ func SslList(page, size, status, expireStart, expireEnd int, sni string) (int, [
 		db = db.Where("validity_end <= ? ", expireEnd)
 	}
 
-	if err := db.Order("validity_end desc").Offset((page - 1) * size).Limit(size).Find(&sslList).Error; err != nil {
+	sortType = strings.ToLower(sortType)
+	if sortType != "desc" {
+		sortType = "asc"
+	}
+
+	if err := db.Order("validity_end " + sortType).Offset((page - 1) * size).Limit(size).Find(&sslList).Error; err != nil {
 		e := errno.New(errno.DBReadError, err.Error())
 		return 0, nil, e
 	}
@@ -131,7 +141,11 @@ func SslList(page, size, status, expireStart, expireEnd int, sni string) (int, [
 }
 
 func SslItem(id string) (*SslDto, error) {
+	if id == "" {
+		return nil, errno.New(errno.InvalidParam)
+	}
 	ssl := &Ssl{}
+
 	if err := conf.DB().Table("ssls").Where("id = ?", id).First(ssl).Error; err != nil {
 		e := errno.New(errno.DBReadError, err.Error())
 		return nil, e
@@ -166,53 +180,126 @@ func SslCreate(param interface{}, id string) error {
 	sslReq := &SslRequest{}
 	sslReq.Parse(param)
 
-	ssl, err := ParseCert(sslReq.PublicKey, sslReq.PrivateKey)
+	if sslReq.PrivateKey == "" {
+		return errno.New(errno.InvalidParamDetail, "Key is required")
+	}
+	if sslReq.PublicKey == "" {
+		return errno.New(errno.InvalidParamDetail, "Cert is required")
+	}
 
+	sslReq.PublicKey = strings.TrimSpace(sslReq.PublicKey)
+	sslReq.PrivateKey = strings.TrimSpace(sslReq.PrivateKey)
+
+	ssl, err := ParseCert(sslReq.PublicKey, sslReq.PrivateKey)
 	if err != nil {
 		e := errno.FromMessage(errno.SslParseError, err.Error())
 		return e
 	}
 
-	// first admin api
+	ssl.ID = uuid.FromStringOrNil(id)
+	ssl.Status = 1
+	data := []byte(ssl.PublicKey)
+	hash := md5.Sum(data)
+	ssl.PublicKeyHash = fmt.Sprintf("%x", hash)
+
+	//check hash
+	exists := Ssl{}
+	conf.DB().Table("ssls").Where("public_key_hash = ?", ssl.PublicKeyHash).First(&exists)
+	if exists != (Ssl{}) {
+		e := errno.New(errno.DuplicateSslCert)
+		return e
+	}
+
+	//check sni
 	var snis []string
 	_ = json.Unmarshal([]byte(ssl.Snis), &snis)
 	sslReq.Snis = snis
 
+	// trans
+	tx := conf.DB().Begin()
+	defer func() {
+		if r := recover(); r != nil {
+			tx.Rollback()
+		}
+	}()
+
+	// update mysql
+	if err := tx.Create(ssl).Error; err != nil {
+		tx.Rollback()
+		return errno.New(errno.DBWriteError, err.Error())
+	}
+
+	//admin api
+
 	if _, err := sslReq.PutToApisix(id); err != nil {
+		tx.Rollback()
 		if _, ok := err.(*errno.HttpError); ok {
 			return err
 		}
 		e := errno.New(errno.ApisixSslCreateError, err.Error())
 		return e
 	}
-	// then mysql
-	ssl.ID = uuid.FromStringOrNil(id)
-	ssl.Status = 1
 
-	if err := conf.DB().Create(ssl).Error; err != nil {
-		return errno.New(errno.DBWriteError, err.Error())
-	}
+	tx.Commit()
 
 	return nil
 }
 
 func SslUpdate(param interface{}, id string) error {
+	if id == "" {
+		return errno.New(errno.InvalidParam)
+	}
+
 	sslReq := &SslRequest{}
 	sslReq.Parse(param)
 
-	ssl, err := ParseCert(sslReq.PublicKey, sslReq.PrivateKey)
+	if sslReq.PrivateKey == "" {
+		return errno.New(errno.InvalidParamDetail, "Key is required")
+	}
+	if sslReq.PublicKey == "" {
+		return errno.New(errno.InvalidParamDetail, "Cert is required")
+	}
 
+	ssl, err := ParseCert(sslReq.PublicKey, sslReq.PrivateKey)
 	if err != nil {
-		e := errno.FromMessage(errno.SslParseError, err.Error())
+		return errno.FromMessage(errno.SslParseError, err.Error())
+	}
+
+	hash := md5.Sum([]byte(ssl.PublicKey))
+	ssl.ID = uuid.FromStringOrNil(id)
+	ssl.PublicKeyHash = fmt.Sprintf("%x", hash)
+
+	//check hash
+	exists := Ssl{}
+	conf.DB().Table("ssls").Where("public_key_hash = ?", ssl.PublicKeyHash).First(&exists)
+	if exists != (Ssl{}) && exists.ID != ssl.ID {
+		e := errno.New(errno.DuplicateSslCert)
 		return e
 	}
 
-	// first admin api
+	// trans
+	tx := conf.DB().Begin()
+	defer func() {
+		if r := recover(); r != nil {
+			tx.Rollback()
+		}
+	}()
+
+	//sni check
 	var snis []string
 	_ = json.Unmarshal([]byte(ssl.Snis), &snis)
 	sslReq.Snis = snis
 
+	// update mysql
+	data := Ssl{PublicKey: ssl.PublicKey, Snis: ssl.Snis, ValidityStart: ssl.ValidityStart, ValidityEnd: ssl.ValidityEnd}
+	if err := tx.Model(&ssl).Updates(data).Error; err != nil {
+		tx.Rollback()
+		return errno.New(errno.DBWriteError, err.Error())
+	}
+
+	//admin api
 	if _, err := sslReq.PutToApisix(id); err != nil {
+		tx.Rollback()
 		if _, ok := err.(*errno.HttpError); ok {
 			return err
 		}
@@ -220,21 +307,37 @@ func SslUpdate(param interface{}, id string) error {
 		return e
 	}
 
-	// then mysql
-	ssl.ID = uuid.FromStringOrNil(id)
-	data := Ssl{PublicKey: ssl.PublicKey, Snis: ssl.Snis, ValidityStart: ssl.ValidityStart, ValidityEnd: ssl.ValidityEnd}
-	if err := conf.DB().Model(&ssl).Updates(data).Error; err != nil {
-		return errno.New(errno.DBWriteError, err.Error())
-	}
+	tx.Commit()
 
 	return nil
 }
 
 func SslPatch(param interface{}, id string) error {
+	if id == "" {
+		return errno.New(errno.InvalidParam)
+	}
+
 	sslReq := &SslRequest{}
 	sslReq.Parse(param)
 
+	// trans
+	tx := conf.DB().Begin()
+	defer func() {
+		if r := recover(); r != nil {
+			tx.Rollback()
+		}
+	}()
+
+	// update mysql
+	ssl := Ssl{}
+	ssl.ID = uuid.FromStringOrNil(id)
+	if err := tx.Model(&ssl).Update("status", sslReq.Status).Error; err != nil {
+		tx.Rollback()
+		return errno.New(errno.DBWriteError, err.Error())
+	}
+
 	if _, err := sslReq.PatchToApisix(id); err != nil {
+		tx.Rollback()
 		if _, ok := err.(*errno.HttpError); ok {
 			return err
 		}
@@ -242,32 +345,45 @@ func SslPatch(param interface{}, id string) error {
 		return e
 	}
 
-	ssl := Ssl{}
-	ssl.ID = uuid.FromStringOrNil(id)
-	if err := conf.DB().Model(&ssl).Update("status", sslReq.Status).Error; err != nil {
-		return errno.New(errno.DBWriteError, err.Error())
-	}
+	tx.Commit()
 
 	return nil
 }
 
 func SslDelete(id string) error {
+	if id == "" {
+		return errno.New(errno.InvalidParam)
+	}
+
+	// trans
+	tx := conf.DB().Begin()
+	defer func() {
+		if r := recover(); r != nil {
+			tx.Rollback()
+		}
+	}()
+
+	// delete from mysql
+	ssl := &Ssl{}
+	ssl.ID = uuid.FromStringOrNil(id)
+	if err := conf.DB().Delete(ssl).Error; err != nil {
+		tx.Rollback()
+		return errno.New(errno.DBDeleteError, err.Error())
+	}
+
 	// delete from apisix
 	request := &SslRequest{}
 	request.ID = id
 	if _, err := request.DeleteFromApisix(); err != nil {
+		tx.Rollback()
 		if _, ok := err.(*errno.HttpError); ok {
 			return err
 		}
 		e := errno.New(errno.ApisixSslDeleteError, err.Error())
 		return e
 	}
-	// delete from mysql
-	ssl := &Ssl{}
-	ssl.ID = uuid.FromStringOrNil(id)
-	if err := conf.DB().Delete(ssl).Error; err != nil {
-		return errno.New(errno.DBDeleteError, err.Error())
-	}
+
+	tx.Commit()
 
 	return nil
 }
@@ -404,3 +520,70 @@ func ParseCert(crt, key string) (*Ssl, error) {
 		return &ssl, nil
 	}
 }
+
+func CheckSniExists(param interface{}) error {
+	var hosts []string
+	if err := json.Unmarshal(param.([]byte), &hosts); err != nil {
+		return errno.FromMessage(errno.InvalidParam)
+	}
+
+	sslList := []Ssl{}
+	db := conf.DB().Table("ssls")
+	db = db.Where("`status` = ? ", 1)
+
+	condition := ""
+	args := []interface{}{}
+	first := true
+	for _, host := range hosts {
+		idx := strings.Index(host, "*")
+		keyword := strings.Replace(host, "*.", "", -1)
+		if idx == -1 {
+			if j := strings.Index(host, "."); j != -1 {
+				keyword = host[j:]
+				//just one `.`
+				if j := strings.Index(host[(j+1):], "."); j == -1 {
+					keyword = host
+				}
+			}
+		}
+		if first {
+			condition = condition + "`snis` like ?"
+		} else {
+			condition = condition + " or `snis` like ?"
+		}
+		first = false
+		args = append(args, "%"+keyword+"%")
+	}
+	db = db.Where(condition, args...)
+
+	if err := db.Find(&sslList).Error; err != nil {
+		return errno.FromMessage(errno.SslForSniNotExists, hosts[0])
+	}
+
+hre:
+	for _, host := range hosts {
+		for _, ssl := range sslList {
+			sslDto := SslDto{}
+			sslDto.Parse(&ssl)
+			for _, sni := range sslDto.Snis {
+				if sni == host {
+					continue hre
+				}
+				regx := strings.Replace(sni, ".", `\.`, -1)
+				regx = strings.Replace(regx, "*", `([^\.]+)`, -1)
+				regx = "^" + regx + "$"
+				if isOk, _ := regexp.MatchString(regx, host); isOk {
+					continue hre
+				}
+			}
+		}
+		return errno.FromMessage(errno.SslForSniNotExists, host)
+	}
+
+	return nil
+}
+
+func DeleteTestSslData() {
+	db := conf.DB().Table("ssls")
+	db.Where("snis LIKE ? OR (snis LIKE ? AND snis LIKE ? )", "%*.route.com%", "%r.com%", "%s.com%").Delete(Ssl{})
+}
diff --git a/api/service/ssl_test.go b/api/service/ssl_test.go
index 46607e8..46ecbb9 100644
--- a/api/service/ssl_test.go
+++ b/api/service/ssl_test.go
@@ -218,6 +218,16 @@ func TestSslCurd(t *testing.T) {
 		assert.Equal(true, strings.Contains(dm, "test3.com"))
 	}
 
+	//test3.com duplicate
+	param = []byte(`{
+		"cert": "-----BEGIN CERTIFICATE-----\nMIIEWjCCAsKgAwIBAgIRAMLLNCKEvgEQL22Hpox6E1kwDQYJKoZIhvcNAQELBQAw\nfzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSowKAYDVQQLDCFqdW54\ndWNoZW5AanVueHVkZUFpciAoanVueHUgY2hlbikxMTAvBgNVBAMMKG1rY2VydCBq\ndW54dWNoZW5AanVueHVkZUFpciAoanVueHUgY2hlbikwHhcNMTkwNjAxMDAwMDAw\nWhcNMzAwNjA5MTA0MjA1WjBVMScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQg\nY2VydGlmaWNhdGUxKjAoBgNVBAsMIWp1bnh1Y2hlbkBqdW54dWRlQWlyIChqdW54\ndSBjaGVuKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL2l [...]
+		"key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC9pYPr8x5ESArP\nDx7+lFcVxW6y+sJbHbqziApFuXWilCcs1GUGZddQivUljpPLwHO496UOzM9ea4X0\nAeefFh8yP1122dTa2a06S2i9H1luZ+ISBPBG2H9x8v8TPIEVF7FjBHsatlgv0T30\nwkCGfOb1EgDUu9hOax3W2t2OSDK/G5/zVINbJti8WUJmVerQTqWJ4m8o+218cPJM\na+MPgf+YwwT5sC+fW3aGrRLNRY3R88/n21tSuDXR2WRKLWKpEIQktPSZ8BrMHQK4\nsviLOOJLf4LcO51Vprlf3kGsTxB1ROAcL3Zr9dSLD7I+oYn9t4F41yyB8eugmP0u\nCPz9GpZZAgMBAAECggEAZ2+8SVgb/QASLSdBL3d3HB/IJgSRNyM67qrXd [...]
+	}`)
+
+	err = SslCreate(param, u2.String())
+	assert.NotNil(err)
+	assert.Equal(errno.DuplicateSslCert.Code, err.(*errno.ManagerError).Code)
+
 	//a.com b.com fail
 	param = []byte(`{
 		"cert": "-----BEGIN CERTIFICATE-----\nMIICcTCCAdoCCQDQoPEll/bQizANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJD\nTjEOMAwGA1UECAwFbXlrZXkxDjAMBgNVBAcMBW15a2V5MQ4wDAYDVQQKDAVteWtl\neTEOMAwGA1UECwwFbXlrZXkxDjAMBgNVBAMMBWEuY29tMQ4wDAYDVQQDDAViLmNv\nbTEOMAwGA1UEAwwFYy5jb20wHhcNMjAwNjE3MDk1MDA0WhcNMzAwNjE1MDk1MDA0\nWjB9MQswCQYDVQQGEwJDTjEOMAwGA1UECAwFbXlrZXkxDjAMBgNVBAcMBW15a2V5\nMQ4wDAYDVQQKDAVteWtleTEOMAwGA1UECwwFbXlrZXkxDjAMBgNVBAMMBWEuY29t\nMQ4wDAYDVQQDDAViLmNvbTEOMAwGA1UEAwwFYy5jb20wgZ8wDQYJKoZI [...]
@@ -238,10 +248,29 @@ func TestSslCurd(t *testing.T) {
 	assert.Nil(err)
 
 	//list
-	count, list, err := SslList(2, 1, -1, 0, 0, "")
+	count, list, err := SslList(2, 1, -1, 0, 0, "", "asc")
 	assert.Equal(true, count >= 2)
 	assert.Equal(1, len(list))
 
+	// check sni ssl exist
+	param = []byte(`[
+		"test3.com",
+		"www.test3.com",
+		"a.com"
+	]`)
+
+	err = CheckSniExists(param)
+	assert.Nil(err)
+
+	// check sni ssl exist
+	param = []byte(`[
+		"test3.com",
+		"a.test3.com",
+		"b.com"
+	]`)
+	err = CheckSniExists(param)
+	assert.NotNil(err)
+
 	// patch
 	param = []byte(`{
 		"status": 0
@@ -252,23 +281,70 @@ func TestSslCurd(t *testing.T) {
 	ssl, err = SslItem(u1.String())
 	assert.Equal(uint64(0), ssl.Status)
 
+	// check sni ssl exist  --- disable test3
+	param = []byte(`[
+		"test3.com",
+		"www.test3.com",
+		"a.com"
+	]`)
+
+	err = CheckSniExists(param)
+	assert.NotNil(err)
+
+	param = []byte(`[
+		"a.com"
+	]`)
+	err = CheckSniExists(param)
+	assert.Nil(err)
+
 	//update
 	param = []byte(`{
-		"cert": "-----BEGIN CERTIFICATE-----\nMIICcTCCAdoCCQDQoPEll/bQizANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJD\nTjEOMAwGA1UECAwFbXlrZXkxDjAMBgNVBAcMBW15a2V5MQ4wDAYDVQQKDAVteWtl\neTEOMAwGA1UECwwFbXlrZXkxDjAMBgNVBAMMBWEuY29tMQ4wDAYDVQQDDAViLmNv\nbTEOMAwGA1UEAwwFYy5jb20wHhcNMjAwNjE3MDk1MDA0WhcNMzAwNjE1MDk1MDA0\nWjB9MQswCQYDVQQGEwJDTjEOMAwGA1UECAwFbXlrZXkxDjAMBgNVBAcMBW15a2V5\nMQ4wDAYDVQQKDAVteWtleTEOMAwGA1UECwwFbXlrZXkxDjAMBgNVBAMMBWEuY29t\nMQ4wDAYDVQQDDAViLmNvbTEOMAwGA1UEAwwFYy5jb20wgZ8wDQYJKoZI [...]
-		"key": "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQDRzKypXxcyW8mLg9GL3itpjjl0fc14vrwuZVukWqMio8eQC8iF\n0Fl1ERlZ8cNfsW5sVjdTF2cgD+3iWBT2tDyCb1tJG8h5Cbf06wsgS1TqqU6rxJAw\nCHXvRcfN+eerFw+Nzqvd5cga2l5kfn17qC8SUEC4QC1tbToNV91a8SmFzQIDAQAB\nAoGBAJIL/y4wqf8+ckES1G6fjG0AuvJjGQQzEuDhYjg5eFMG3EdkTIUKkxuxeYpp\niG43H/1+zyiipAFn1Vu5oW5T7cJEgC1YA39dERT605S5BrNWWHoZsgH+qmLoq7X+\njXMlmCagwlgwhUWMU2M1/LUbAl42384dK9u3EwcCgS//sFuBAkEA6mK52/Z03PB3\n0sS14eN7xFl96yc/NcneJ7Vy5APT0KGLo0j2S8gpOVW9EYrrzDzWg [...]
+		"cert": "-----BEGIN CERTIFICATE-----\nMIIEWzCCAsOgAwIBAgIQDYoN+el2w074sSGlyKVZFTANBgkqhkiG9w0BAQsFADB/\nMR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExKjAoBgNVBAsMIWp1bnh1\nY2hlbkBqdW54dWRlQWlyIChqdW54dSBjaGVuKTExMC8GA1UEAwwobWtjZXJ0IGp1\nbnh1Y2hlbkBqdW54dWRlQWlyIChqdW54dSBjaGVuKTAeFw0xOTA2MDEwMDAwMDBa\nFw0zMDA3MDQwNjA0MzNaMFUxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBj\nZXJ0aWZpY2F0ZTEqMCgGA1UECwwhanVueHVjaGVuQGp1bnh1ZGVBaXIgKGp1bnh1\nIGNoZW4pMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy21L [...]
+		"key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDLbUtgWZBileYy\nVSPJvBsnffybAqbFj7BMQksGHDu0Fxq71oWOPI7hpGmHiul0xCAKfiXBMwTaVsV1\nGEfNTn5LL0mwjWT738nyxlJpMiP2lo5fY9icQ6gWMjH3CklGl4s0GPLcgJLccLhL\nNNsNUCT0tkgsu//zW+3UqlDUeg+kT45nXBf4IvT/pGc2Z1qqCua20+/Sy6X3Ih4r\nPX0evbqnHuDpSegrSjytLAKpWaOhVMn55jbOabNKGoU9MsWC81mYx4vd0+aDqcZ2\nOOxuRdLnS2HJMNv+H5gi8Sxl+sNWBtXaDo/8qLJ8oLJ68xDF3Unn9QjgeW0jcxeC\numhqIOJXAgMBAAECggEARdPea9RSm4SY3+4ZusW3DHdSnmLqnCYWfhbDa [...]
 	}`)
 
 	err = SslUpdate(param, u1.String())
 	assert.Nil(err)
 
 	ssl, _ = SslItem(u1.String())
-	assert.Equal(3, len(ssl.Snis))
+	assert.Equal(2, len(ssl.Snis))
+
+	// check sni ssl exist
+	param = []byte(`[
+		"example.com",
+		"www.example.com",
+		"a.example.com",
+		"a.com",
+		"b.com"
+	]`)
+
+	err = CheckSniExists(param)
+	assert.NotNil(err)
+	assert.Equal(errno.SslForSniNotExists.Code, err.(*errno.ManagerError).Code)
+
+	param = []byte(`{
+		"status": 1
+	}`)
+	err = SslPatch(param, u1.String())
+	assert.Nil(err)
+
+	// check sni ssl exist
+	param = []byte(`[
+		"example.com",
+		"www.example.com",
+		"a.example.com",
+		"a.com",
+		"b.com"
+	]`)
+
+	err = CheckSniExists(param)
+	assert.Nil(err)
 
 	//delete
 	err = SslDelete(u1.String())
 	assert.Nil(err)
 
-	count2, _, err := SslList(2, 1, -1, 0, 0, "")
+	count2, _, err := SslList(2, 1, -1, 0, 0, "", "desc")
 	assert.Equal(count2, count-1)
 
 	err = SslDelete(u2.String())