diff --git a/README.md b/README.md index 63d7898b..a5b3ac6f 100644 --- a/README.md +++ b/README.md @@ -71,22 +71,22 @@ make local.run ## 三、支持的服务商列表 -| 服务商 | 支持申请证书 | 支持部署证书 | 备注 | -| :--------: | :----------: | :----------: | ------------------------------------------------------------ | -| 阿里云 | √ | √ | 可签发在阿里云注册的域名;可部署到阿里云 OSS、CDN | -| 腾讯云 | √ | √ | 可签发在腾讯云注册的域名;可部署到腾讯云 COS、CDN、CLB | -| 华为云 | √ | √ | 可签发在华为云注册的域名;可部署到华为云 CDN、ELB | -| 七牛云 | | √ | 可部署到七牛云 CDN | -| AWS | √ | | 可签发在 AWS Route53 托管的域名 | -| CloudFlare | √ | | 可签发在 CloudFlare 注册的域名;CloudFlare 服务自带 SSL 证书 | -| GoDaddy | √ | | 可签发在 GoDaddy 注册的域名 | -| Namesilo | √ | | 可签发在 Namesilo 注册的域名 | -| PowerDNS | √ | | 可签发在 PowerDNS 托管的域名 | -| HTTP 请求 | √ | | 可签发允许通过 HTTP 请求修改 DNS 的域名 | -| 本地部署 | | √ | 可部署到本地服务器 | -| SSH | | √ | 可部署到 SSH 服务器 | -| Webhook | | √ | 可部署时回调到 Webhook | -| Kubernetes | | √ | 可部署到 Kubernetes Secret | +| 服务商 | 支持申请证书 | 支持部署证书 | 备注 | +| :--------: | :----------: | :----------: | ----------------------------------------------------------------- | +| 阿里云 | √ | √ | 可签发在阿里云注册的域名;可部署到阿里云 OSS、CDN、SLB | +| 腾讯云 | √ | √ | 可签发在腾讯云注册的域名;可部署到腾讯云 COS、CDN、ECDN、CLB、TEO | +| 华为云 | √ | √ | 可签发在华为云注册的域名;可部署到华为云 CDN、ELB | +| 七牛云 | | √ | 可部署到七牛云 CDN | +| AWS | √ | | 可签发在 AWS Route53 托管的域名 | +| CloudFlare | √ | | 可签发在 CloudFlare 注册的域名;CloudFlare 服务自带 SSL 证书 | +| GoDaddy | √ | | 可签发在 GoDaddy 注册的域名 | +| Namesilo | √ | | 可签发在 Namesilo 注册的域名 | +| PowerDNS | √ | | 可签发在 PowerDNS 托管的域名 | +| HTTP 请求 | √ | | 可签发允许通过 HTTP 请求修改 DNS 的域名 | +| 本地部署 | | √ | 可部署到本地服务器 | +| SSH | | √ | 可部署到 SSH 服务器 | +| Webhook | | √ | 可部署时回调到 Webhook | +| Kubernetes | | √ | 可部署到 Kubernetes Secret | ## 四、系统截图 @@ -180,3 +180,4 @@ Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE. ## 九、Star 趋势图 [![Stargazers over time](https://starchart.cc/usual2970/certimate.svg?variant=adaptive)](https://starchart.cc/usual2970/certimate) + diff --git a/README_EN.md b/README_EN.md index 5bb30b38..80c8f76c 100644 --- a/README_EN.md +++ b/README_EN.md @@ -59,7 +59,7 @@ make local.run ## Usage -After completing the installation steps above, you can access the Certimate management page by visiting http://127.0.0.1:8090 in your browser. +After completing the installation steps above, you can access the Certimate management page by visiting in your browser. ```bash username:admin@certimate.fun @@ -70,22 +70,22 @@ password:1234567890 ## List of Supported Providers -| Provider | Registration | Deployment | Remarks | -| :-----------: | :----------: | :--------: | ------------------------------------------------------------------------------------------------ | -| Alibaba Cloud | √ | √ | Supports domains registered on Alibaba Cloud; supports deployment to Alibaba Cloud OSS, CDN | -| Tencent Cloud | √ | √ | Supports domains registered on Tencent Cloud; supports deployment to Tencent Cloud COS, CDN, CLB | -| Huawei Cloud | √ | √ | Supports domains registered on Huawei Cloud; supports deployment to Huawei Cloud CDN, ELB | -| Qiniu Cloud | | √ | Supports deployment to Qiniu Cloud CDN | -| AWS | √ | | Supports domains managed on AWS Route53 | -| CloudFlare | √ | | Supports domains registered on CloudFlare; CloudFlare services come with SSL certificates | -| GoDaddy | √ | | Supports domains registered on GoDaddy | -| Namesilo | √ | | Supports domains registered on Namesilo | -| PowerDNS | √ | | Supports domains managed on PowerDNS | -| HTTP Request | √ | | Supports domains which allow managing DNS by HTTP request | -| Local Deploy | | √ | Supports deployment to local servers | -| SSH | | √ | Supports deployment to SSH servers | -| Webhook | | √ | Supports callback to Webhook | -| Kubernetes | | √ | Supports deployment to Kubernetes Secret | +| Provider | Registration | Deployment | Remarks | +| :-----------: | :----------: | :--------: | --------------------------------------------------------------------------------------------------------------------- | +| Alibaba Cloud | √ | √ | Supports domains registered on Alibaba Cloud; supports deployment to Alibaba Cloud OSS, CDN,SLB | +| Tencent Cloud | √ | √ | Supports domains registered on Tencent Cloud; supports deployment to Tencent Cloud COS, CDN, ECDN, CLB, TEO | +| Huawei Cloud | √ | √ | Supports domains registered on Huawei Cloud; supports deployment to Huawei Cloud CDN, ELB | +| Qiniu Cloud | | √ | Supports deployment to Qiniu Cloud CDN | +| AWS | √ | | Supports domains managed on AWS Route53 | +| CloudFlare | √ | | Supports domains registered on CloudFlare; CloudFlare services come with SSL certificates | +| GoDaddy | √ | | Supports domains registered on GoDaddy | +| Namesilo | √ | | Supports domains registered on Namesilo | +| PowerDNS | √ | | Supports domains managed on PowerDNS | +| HTTP Request | √ | | Supports domains which allow managing DNS by HTTP request | +| Local Deploy | | √ | Supports deployment to local servers | +| SSH | | √ | Supports deployment to SSH servers | +| Webhook | | √ | Supports callback to Webhook | +| Kubernetes | | √ | Supports deployment to Kubernetes Secret | ## Screenshots diff --git a/go.mod b/go.mod index 86e87380..d846826f 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,12 @@ go 1.22.0 toolchain go1.23.2 require ( + github.com/alibabacloud-go/alb-20200616/v2 v2.2.1 github.com/alibabacloud-go/cas-20200407/v3 v3.0.1 github.com/alibabacloud-go/cdn-20180510/v5 v5.0.0 github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 + github.com/alibabacloud-go/nlb-20220430/v2 v2.0.3 + github.com/alibabacloud-go/slb-20140515/v4 v4.0.9 github.com/alibabacloud-go/tea v1.2.2 github.com/alibabacloud-go/tea-utils/v2 v2.0.6 github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible @@ -21,7 +24,7 @@ require ( github.com/pocketbase/pocketbase v0.22.18 github.com/qiniu/go-sdk/v7 v7.22.0 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1017 - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1017 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1030 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992 golang.org/x/crypto v0.28.0 k8s.io/api v0.31.1 @@ -56,6 +59,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/technoweenie/multipartstreamer v1.0.1 // indirect + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.0.1030 // indirect github.com/x448/float16 v0.8.4 // indirect go.mongodb.org/mongo-driver v1.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 0ee4a995..fdcb5b6f 100644 --- a/go.sum +++ b/go.sum @@ -29,6 +29,8 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDe github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI= +github.com/alibabacloud-go/alb-20200616/v2 v2.2.1 h1:b8ixnrkFhWrmJQd+iEE1UWPD5vdyC3d9l7G0uvkfi2s= +github.com/alibabacloud-go/alb-20200616/v2 v2.2.1/go.mod h1:cPdZwovbqpv+5nM/HnMwZpG5q0/gBuX31hu2H1VoyrM= github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA= github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo= github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= @@ -45,6 +47,8 @@ github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc= github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc= github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.0/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.5/go.mod h1:kUe8JqFmoVU7lfBauaDD5taFaW7mBI+xVsyHutYtabg= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.7/go.mod h1:CzQnh+94WDnJOnKZH5YRyouL+OOcdBnXY5VWAf0McgI= github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.8/go.mod h1:CzQnh+94WDnJOnKZH5YRyouL+OOcdBnXY5VWAf0McgI= github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.9/go.mod h1:bb+Io8Sn2RuM3/Rpme6ll86jMyFSrD1bxeV/+v61KeU= github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 h1:GEYkMApgpKEVDn6z12DcH1EGYpDYRB8JxsazM4Rywak= @@ -61,11 +65,15 @@ github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc= github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q= github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE= +github.com/alibabacloud-go/nlb-20220430/v2 v2.0.3 h1:LtyUVlgBEKyzWgQJurzXM6MXCt84sQr9cE5OKqYymko= +github.com/alibabacloud-go/nlb-20220430/v2 v2.0.3/go.mod h1:4a/RcBYeAhYowHzX+LMgnouz7NradnSKPKl14KS3B1U= github.com/alibabacloud-go/openapi-util v0.0.11/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY= github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1 h1:L0TIjr9Qh/SLVc1yPhFkcB9+9SbCNK/jPq4ZKB5zmnc= github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1/go.mod h1:EKxBRDLcMzwl4VLF/1WJwlByZZECJawPXUvinKMsTTs= +github.com/alibabacloud-go/slb-20140515/v4 v4.0.9 h1:nrf9gQth7fONUj7V8i78Yb98eb9NdKl0VdeSjmeYugI= +github.com/alibabacloud-go/slb-20140515/v4 v4.0.9/go.mod h1:PEMEsQoxhkMvykMFP5ZXg6SWI9vmAiZ6lK3Pu4mTKB0= github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg= github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= @@ -89,6 +97,7 @@ github.com/alibabacloud-go/tea-utils v1.3.6/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQ github.com/alibabacloud-go/tea-utils v1.4.5 h1:h0/6Xd2f3bPE4XHTvkpjwxowIwRCJAJOqY6Eq8f3zfA= github.com/alibabacloud-go/tea-utils v1.4.5/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw= github.com/alibabacloud-go/tea-utils/v2 v2.0.0/go.mod h1:U5MTY10WwlquGPS34DOeomUGBB0gXbLueiq5Trwu0C4= +github.com/alibabacloud-go/tea-utils/v2 v2.0.4/go.mod h1:sj1PbjPodAVTqGTA3olprfeeqqmwD0A5OQz94o9EuXQ= github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4= github.com/alibabacloud-go/tea-utils/v2 v2.0.6 h1:ZkmUlhlQbaDC+Eba/GARMPy6hKdCLiSke5RsN5LcyQ0= github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= @@ -451,10 +460,14 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.992/go.mod github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1017 h1:SXrldOXwgomYuATVAuz5ofpTjB+99qVELgdy5R5kMgI= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1017/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1030 h1:kwiUoCkooUgy7iPyhEEbio7WT21kGJUeZ5JeJfb/dYk= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1030/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 h1:QwE0dRkAAbdf+eACnkNULgDn9ZKUJpPWRyXdqJolP5E= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002/go.mod h1:WdC0FYbqYhJwQ3kbqri6hVP5HAEp+rzX9FToItTAzUg= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992 h1:A6O89OlCJQUpNxGqC/E5By04UNKBryIt5olQIGOx8mg= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992/go.mod h1:BcvC7ZPdSlhRggVq4J1ToJlgv8bmODIAuSo0naFZOLo= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.0.1030 h1:tlHbfQlAfL12J/5XF4indKl0cAA3vEn6TDiGZVsr050= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.0.1030/go.mod h1:8dW6JByZKNDAPnjlXxBk9yDc+QGbldpa0tBRfi1kG+U= github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go index 17e97cb5..128704b4 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -98,7 +98,7 @@ func newApplyUser(ca, email string) (*ApplyUser, error) { if err != nil { return nil, err } - keyStr, err := x509.PrivateKeyToPEM(privateKey) + keyStr, err := x509.ConvertECPrivateKeyToPEM(privateKey) if err != nil { return nil, err } @@ -122,7 +122,7 @@ func (u ApplyUser) GetRegistration() *registration.Resource { } func (u *ApplyUser) GetPrivateKey() crypto.PrivateKey { - rs, _ := x509.ParsePrivateKeyFromPEM(u.key) + rs, _ := x509.ParseECPrivateKeyFromPEM(u.key) return rs } diff --git a/internal/deployer/aliyun_alb.go b/internal/deployer/aliyun_alb.go new file mode 100644 index 00000000..b676e043 --- /dev/null +++ b/internal/deployer/aliyun_alb.go @@ -0,0 +1,265 @@ +package deployer + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + alb20200616 "github.com/alibabacloud-go/alb-20200616/v2/client" + openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" + "github.com/alibabacloud-go/tea/tea" + + "github.com/usual2970/certimate/internal/domain" + "github.com/usual2970/certimate/internal/pkg/core/uploader" +) + +type AliyunALBDeployer struct { + option *DeployerOption + infos []string + + sdkClient *alb20200616.Client + sslUploader uploader.Uploader +} + +func NewAliyunALBDeployer(option *DeployerOption) (Deployer, error) { + access := &domain.AliyunAccess{} + json.Unmarshal([]byte(option.Access), access) + + client, err := (&AliyunALBDeployer{}).createSdkClient( + access.AccessKeyId, + access.AccessKeySecret, + option.DeployConfig.GetConfigAsString("region"), + ) + if err != nil { + return nil, err + } + + uploader, err := uploader.NewAliyunCASUploader(&uploader.AliyunCASUploaderConfig{ + AccessKeyId: access.AccessKeyId, + AccessKeySecret: access.AccessKeySecret, + Region: option.DeployConfig.GetConfigAsString("region"), + }) + if err != nil { + return nil, err + } + + return &AliyunALBDeployer{ + option: option, + infos: make([]string, 0), + sdkClient: client, + sslUploader: uploader, + }, nil +} + +func (d *AliyunALBDeployer) GetID() string { + return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) +} + +func (d *AliyunALBDeployer) GetInfo() []string { + return d.infos +} + +func (d *AliyunALBDeployer) Deploy(ctx context.Context) error { + switch d.option.DeployConfig.GetConfigAsString("resourceType") { + case "loadbalancer": + if err := d.deployToLoadbalancer(ctx); err != nil { + return err + } + case "listener": + if err := d.deployToListener(ctx); err != nil { + return err + } + default: + return errors.New("unsupported resource type") + } + + return nil +} + +func (d *AliyunALBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*alb20200616.Client, error) { + if region == "" { + region = "cn-hangzhou" // ALB 服务默认区域:华东一杭州 + } + + aConfig := &openapi.Config{ + AccessKeyId: tea.String(accessKeyId), + AccessKeySecret: tea.String(accessKeySecret), + } + + var endpoint string + switch region { + case "cn-hangzhou-finance": + endpoint = "alb.cn-hangzhou.aliyuncs.com" + default: + endpoint = fmt.Sprintf("alb.%s.aliyuncs.com", region) + } + aConfig.Endpoint = tea.String(endpoint) + + client, err := alb20200616.NewClient(aConfig) + if err != nil { + return nil, err + } + + return client, nil +} + +func (d *AliyunALBDeployer) deployToLoadbalancer(ctx context.Context) error { + aliLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId") + if aliLoadbalancerId == "" { + return errors.New("`loadbalancerId` is required") + } + + aliListenerIds := make([]string, 0) + + // 查询负载均衡实例的详细信息 + // REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getloadbalancerattribute + getLoadBalancerAttributeReq := &alb20200616.GetLoadBalancerAttributeRequest{ + LoadBalancerId: tea.String(aliLoadbalancerId), + } + getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'alb.GetLoadBalancerAttribute': %w", err) + } + + d.infos = append(d.infos, toStr("已查询到 ALB 负载均衡实例", getLoadBalancerAttributeResp)) + + // 查询 HTTPS 监听列表 + // REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners + listListenersPage := 1 + listListenersLimit := int32(100) + var listListenersToken *string = nil + for { + listListenersReq := &alb20200616.ListListenersRequest{ + MaxResults: tea.Int32(listListenersLimit), + NextToken: listListenersToken, + LoadBalancerIds: []*string{tea.String(aliLoadbalancerId)}, + ListenerProtocol: tea.String("HTTPS"), + } + listListenersResp, err := d.sdkClient.ListListeners(listListenersReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'alb.ListListeners': %w", err) + } + + if listListenersResp.Body.Listeners != nil { + for _, listener := range listListenersResp.Body.Listeners { + aliListenerIds = append(aliListenerIds, *listener.ListenerId) + } + } + + if listListenersResp.Body.NextToken == nil { + break + } else { + listListenersToken = listListenersResp.Body.NextToken + listListenersPage += 1 + } + } + + d.infos = append(d.infos, toStr("已查询到 ALB 负载均衡实例下的全部 HTTPS 监听", aliListenerIds)) + + // 查询 QUIC 监听列表 + // REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners + listListenersPage = 1 + listListenersToken = nil + for { + listListenersReq := &alb20200616.ListListenersRequest{ + MaxResults: tea.Int32(listListenersLimit), + NextToken: listListenersToken, + LoadBalancerIds: []*string{tea.String(aliLoadbalancerId)}, + ListenerProtocol: tea.String("QUIC"), + } + listListenersResp, err := d.sdkClient.ListListeners(listListenersReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'alb.ListListeners': %w", err) + } + + if listListenersResp.Body.Listeners != nil { + for _, listener := range listListenersResp.Body.Listeners { + aliListenerIds = append(aliListenerIds, *listener.ListenerId) + } + } + + if listListenersResp.Body.NextToken == nil { + break + } else { + listListenersToken = listListenersResp.Body.NextToken + listListenersPage += 1 + } + } + + d.infos = append(d.infos, toStr("已查询到 ALB 负载均衡实例下的全部 QUIC 监听", aliListenerIds)) + + // 上传证书到 SSL + uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + if err != nil { + return err + } + + d.infos = append(d.infos, toStr("已上传证书", uploadResult)) + + // 批量更新监听证书 + var errs []error + for _, aliListenerId := range aliListenerIds { + if err := d.updateListenerCertificate(ctx, aliListenerId, uploadResult.CertId); err != nil { + errs = append(errs, err) + } + } + if len(errs) > 0 { + return errors.Join(errs...) + } + + return nil +} + +func (d *AliyunALBDeployer) deployToListener(ctx context.Context) error { + aliListenerId := d.option.DeployConfig.GetConfigAsString("listenerId") + if aliListenerId == "" { + return errors.New("`listenerId` is required") + } + + // 上传证书到 SSL + uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + if err != nil { + return err + } + + d.infos = append(d.infos, toStr("已上传证书", uploadResult)) + + // 更新监听 + if err := d.updateListenerCertificate(ctx, aliListenerId, uploadResult.CertId); err != nil { + return err + } + + return nil +} + +func (d *AliyunALBDeployer) updateListenerCertificate(ctx context.Context, aliListenerId string, aliCertId string) error { + // 查询监听的属性 + // REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getlistenerattribute + getListenerAttributeReq := &alb20200616.GetListenerAttributeRequest{ + ListenerId: tea.String(aliListenerId), + } + getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'alb.GetListenerAttribute': %w", err) + } + + d.infos = append(d.infos, toStr("已查询到 ALB 监听配置", getListenerAttributeResp)) + + // 修改监听的属性 + // REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-updatelistenerattribute + updateListenerAttributeReq := &alb20200616.UpdateListenerAttributeRequest{ + ListenerId: tea.String(aliListenerId), + Certificates: []*alb20200616.UpdateListenerAttributeRequestCertificates{{ + CertificateId: tea.String(aliCertId), + }}, + } + updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'alb.UpdateListenerAttribute': %w", err) + } + + d.infos = append(d.infos, toStr("已更新 ALB 监听配置", updateListenerAttributeResp)) + + return nil +} diff --git a/internal/deployer/aliyun_clb.go b/internal/deployer/aliyun_clb.go new file mode 100644 index 00000000..11384ba8 --- /dev/null +++ b/internal/deployer/aliyun_clb.go @@ -0,0 +1,282 @@ +package deployer + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" + slb20140515 "github.com/alibabacloud-go/slb-20140515/v4/client" + "github.com/alibabacloud-go/tea/tea" + + "github.com/usual2970/certimate/internal/domain" + "github.com/usual2970/certimate/internal/pkg/core/uploader" +) + +type AliyunCLBDeployer struct { + option *DeployerOption + infos []string + + sdkClient *slb20140515.Client + sslUploader uploader.Uploader +} + +func NewAliyunCLBDeployer(option *DeployerOption) (Deployer, error) { + access := &domain.AliyunAccess{} + json.Unmarshal([]byte(option.Access), access) + + client, err := (&AliyunCLBDeployer{}).createSdkClient( + access.AccessKeyId, + access.AccessKeySecret, + option.DeployConfig.GetConfigAsString("region"), + ) + if err != nil { + return nil, err + } + + uploader, err := uploader.NewAliyunSLBUploader(&uploader.AliyunSLBUploaderConfig{ + AccessKeyId: access.AccessKeyId, + AccessKeySecret: access.AccessKeySecret, + Region: option.DeployConfig.GetConfigAsString("region"), + }) + if err != nil { + return nil, err + } + + return &AliyunCLBDeployer{ + option: option, + infos: make([]string, 0), + sdkClient: client, + sslUploader: uploader, + }, nil +} + +func (d *AliyunCLBDeployer) GetID() string { + return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) +} + +func (d *AliyunCLBDeployer) GetInfo() []string { + return d.infos +} + +func (d *AliyunCLBDeployer) Deploy(ctx context.Context) error { + switch d.option.DeployConfig.GetConfigAsString("resourceType") { + case "loadbalancer": + if err := d.deployToLoadbalancer(ctx); err != nil { + return err + } + case "listener": + if err := d.deployToListener(ctx); err != nil { + return err + } + default: + return errors.New("unsupported resource type") + } + + return nil +} + +func (d *AliyunCLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*slb20140515.Client, error) { + if region == "" { + region = "cn-hangzhou" // CLB(SLB) 服务默认区域:华东一杭州 + } + + aConfig := &openapi.Config{ + AccessKeyId: tea.String(accessKeyId), + AccessKeySecret: tea.String(accessKeySecret), + } + + var endpoint string + switch region { + case "cn-hangzhou": + case "cn-hangzhou-finance": + case "cn-shanghai-finance-1": + case "cn-shenzhen-finance-1": + endpoint = "slb.aliyuncs.com" + default: + endpoint = fmt.Sprintf("slb.%s.aliyuncs.com", region) + } + aConfig.Endpoint = tea.String(endpoint) + + client, err := slb20140515.NewClient(aConfig) + if err != nil { + return nil, err + } + + return client, nil +} + +func (d *AliyunCLBDeployer) deployToLoadbalancer(ctx context.Context) error { + aliLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId") + if aliLoadbalancerId == "" { + return errors.New("`loadbalancerId` is required") + } + + aliListenerPorts := make([]int32, 0) + + // 查询负载均衡实例的详细信息 + // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerattribute + describeLoadBalancerAttributeReq := &slb20140515.DescribeLoadBalancerAttributeRequest{ + RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")), + LoadBalancerId: tea.String(aliLoadbalancerId), + } + describeLoadBalancerAttributeResp, err := d.sdkClient.DescribeLoadBalancerAttribute(describeLoadBalancerAttributeReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerAttribute': %w", err) + } + + d.infos = append(d.infos, toStr("已查询到 CLB 负载均衡实例", describeLoadBalancerAttributeResp)) + + // 查询 HTTPS 监听列表 + // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerlisteners + listListenersPage := 1 + listListenersLimit := int32(100) + var listListenersToken *string = nil + for { + describeLoadBalancerListenersReq := &slb20140515.DescribeLoadBalancerListenersRequest{ + RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")), + MaxResults: tea.Int32(listListenersLimit), + NextToken: listListenersToken, + LoadBalancerId: []*string{tea.String(aliLoadbalancerId)}, + ListenerProtocol: tea.String("https"), + } + describeLoadBalancerListenersResp, err := d.sdkClient.DescribeLoadBalancerListeners(describeLoadBalancerListenersReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerListeners': %w", err) + } + + if describeLoadBalancerListenersResp.Body.Listeners != nil { + for _, listener := range describeLoadBalancerListenersResp.Body.Listeners { + aliListenerPorts = append(aliListenerPorts, *listener.ListenerPort) + } + } + + if describeLoadBalancerListenersResp.Body.NextToken == nil { + break + } else { + listListenersToken = describeLoadBalancerListenersResp.Body.NextToken + listListenersPage += 1 + } + } + + d.infos = append(d.infos, toStr("已查询到 CLB 负载均衡实例下的全部 HTTPS 监听", aliListenerPorts)) + + // 上传证书到 SLB + uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + if err != nil { + return err + } + + d.infos = append(d.infos, toStr("已上传证书", uploadResult)) + + // 批量更新监听证书 + var errs []error + for _, aliListenerPort := range aliListenerPorts { + if err := d.updateListenerCertificate(ctx, aliLoadbalancerId, aliListenerPort, uploadResult.CertId); err != nil { + errs = append(errs, err) + } + } + if len(errs) > 0 { + return errors.Join(errs...) + } + + return nil +} + +func (d *AliyunCLBDeployer) deployToListener(ctx context.Context) error { + aliLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId") + if aliLoadbalancerId == "" { + return errors.New("`loadbalancerId` is required") + } + + aliListenerPort := d.option.DeployConfig.GetConfigAsInt32("listenerPort") + if aliListenerPort == 0 { + return errors.New("`listenerPort` is required") + } + + // 上传证书到 SLB + uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + if err != nil { + return err + } + + d.infos = append(d.infos, toStr("已上传证书", uploadResult)) + + // 更新监听 + if err := d.updateListenerCertificate(ctx, aliLoadbalancerId, aliListenerPort, uploadResult.CertId); err != nil { + return err + } + + return nil +} + +func (d *AliyunCLBDeployer) updateListenerCertificate(ctx context.Context, aliLoadbalancerId string, aliListenerPort int32, aliCertId string) error { + // 查询监听配置 + // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerhttpslistenerattribute + describeLoadBalancerHTTPSListenerAttributeReq := &slb20140515.DescribeLoadBalancerHTTPSListenerAttributeRequest{ + LoadBalancerId: tea.String(aliLoadbalancerId), + ListenerPort: tea.Int32(aliListenerPort), + } + describeLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.DescribeLoadBalancerHTTPSListenerAttribute(describeLoadBalancerHTTPSListenerAttributeReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerHTTPSListenerAttribute': %w", err) + } + + d.infos = append(d.infos, toStr("已查询到 CLB HTTPS 监听配置", describeLoadBalancerHTTPSListenerAttributeResp)) + + // 查询扩展域名 + // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describedomainextensions + describeDomainExtensionsReq := &slb20140515.DescribeDomainExtensionsRequest{ + RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")), + LoadBalancerId: tea.String(aliLoadbalancerId), + ListenerPort: tea.Int32(aliListenerPort), + } + describeDomainExtensionsResp, err := d.sdkClient.DescribeDomainExtensions(describeDomainExtensionsReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'slb.DescribeDomainExtensions': %w", err) + } + + d.infos = append(d.infos, toStr("已查询到 CLB 扩展域名", describeDomainExtensionsResp)) + + // 遍历修改扩展域名 + // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setdomainextensionattribute + // + // 这里仅修改跟被替换证书一致的扩展域名 + if describeDomainExtensionsResp.Body.DomainExtensions == nil && describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension == nil { + for _, domainExtension := range describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension { + if *domainExtension.ServerCertificateId == *describeLoadBalancerHTTPSListenerAttributeResp.Body.ServerCertificateId { + break + } + + setDomainExtensionAttributeReq := &slb20140515.SetDomainExtensionAttributeRequest{ + RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")), + DomainExtensionId: tea.String(*domainExtension.DomainExtensionId), + ServerCertificateId: tea.String(aliCertId), + } + _, err := d.sdkClient.SetDomainExtensionAttribute(setDomainExtensionAttributeReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'slb.SetDomainExtensionAttribute': %w", err) + } + } + } + + // 修改监听配置 + // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setloadbalancerhttpslistenerattribute + // + // 注意修改监听配置要放在修改扩展域名之后 + setLoadBalancerHTTPSListenerAttributeReq := &slb20140515.SetLoadBalancerHTTPSListenerAttributeRequest{ + RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")), + LoadBalancerId: tea.String(aliLoadbalancerId), + ListenerPort: tea.Int32(aliListenerPort), + ServerCertificateId: tea.String(aliCertId), + } + setLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.SetLoadBalancerHTTPSListenerAttribute(setLoadBalancerHTTPSListenerAttributeReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'slb.SetLoadBalancerHTTPSListenerAttribute': %w", err) + } + + d.infos = append(d.infos, toStr("已更新 CLB HTTPS 监听配置", setLoadBalancerHTTPSListenerAttributeResp)) + + return nil +} diff --git a/internal/deployer/aliyun_nlb.go b/internal/deployer/aliyun_nlb.go new file mode 100644 index 00000000..514657e6 --- /dev/null +++ b/internal/deployer/aliyun_nlb.go @@ -0,0 +1,229 @@ +package deployer + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" + nlb20220430 "github.com/alibabacloud-go/nlb-20220430/v2/client" + "github.com/alibabacloud-go/tea/tea" + + "github.com/usual2970/certimate/internal/domain" + "github.com/usual2970/certimate/internal/pkg/core/uploader" +) + +type AliyunNLBDeployer struct { + option *DeployerOption + infos []string + + sdkClient *nlb20220430.Client + sslUploader uploader.Uploader +} + +func NewAliyunNLBDeployer(option *DeployerOption) (Deployer, error) { + access := &domain.AliyunAccess{} + json.Unmarshal([]byte(option.Access), access) + + client, err := (&AliyunNLBDeployer{}).createSdkClient( + access.AccessKeyId, + access.AccessKeySecret, + option.DeployConfig.GetConfigAsString("region"), + ) + if err != nil { + return nil, err + } + + uploader, err := uploader.NewAliyunCASUploader(&uploader.AliyunCASUploaderConfig{ + AccessKeyId: access.AccessKeyId, + AccessKeySecret: access.AccessKeySecret, + Region: option.DeployConfig.GetConfigAsString("region"), + }) + if err != nil { + return nil, err + } + + return &AliyunNLBDeployer{ + option: option, + infos: make([]string, 0), + sdkClient: client, + sslUploader: uploader, + }, nil +} + +func (d *AliyunNLBDeployer) GetID() string { + return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) +} + +func (d *AliyunNLBDeployer) GetInfo() []string { + return d.infos +} + +func (d *AliyunNLBDeployer) Deploy(ctx context.Context) error { + switch d.option.DeployConfig.GetConfigAsString("resourceType") { + case "loadbalancer": + if err := d.deployToLoadbalancer(ctx); err != nil { + return err + } + case "listener": + if err := d.deployToListener(ctx); err != nil { + return err + } + default: + return errors.New("unsupported resource type") + } + + return nil +} + +func (d *AliyunNLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*nlb20220430.Client, error) { + if region == "" { + region = "cn-hangzhou" // NLB 服务默认区域:华东一杭州 + } + + aConfig := &openapi.Config{ + AccessKeyId: tea.String(accessKeyId), + AccessKeySecret: tea.String(accessKeySecret), + } + + var endpoint string + switch region { + default: + endpoint = fmt.Sprintf("nlb.%s.aliyuncs.com", region) + } + aConfig.Endpoint = tea.String(endpoint) + + client, err := nlb20220430.NewClient(aConfig) + if err != nil { + return nil, err + } + + return client, nil +} + +func (d *AliyunNLBDeployer) deployToLoadbalancer(ctx context.Context) error { + aliLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId") + if aliLoadbalancerId == "" { + return errors.New("`loadbalancerId` is required") + } + + aliListenerIds := make([]string, 0) + + // 查询负载均衡实例的详细信息 + // REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getloadbalancerattribute + getLoadBalancerAttributeReq := &nlb20220430.GetLoadBalancerAttributeRequest{ + LoadBalancerId: tea.String(aliLoadbalancerId), + } + getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'nlb.GetLoadBalancerAttribute': %w", err) + } + + d.infos = append(d.infos, toStr("已查询到 NLB 负载均衡实例", getLoadBalancerAttributeResp)) + + // 查询 TCPSSL 监听列表 + // REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-listlisteners + listListenersPage := 1 + listListenersLimit := int32(100) + var listListenersToken *string = nil + for { + listListenersReq := &nlb20220430.ListListenersRequest{ + MaxResults: tea.Int32(listListenersLimit), + NextToken: listListenersToken, + LoadBalancerIds: []*string{tea.String(aliLoadbalancerId)}, + ListenerProtocol: tea.String("TCPSSL"), + } + listListenersResp, err := d.sdkClient.ListListeners(listListenersReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'nlb.ListListeners': %w", err) + } + + if listListenersResp.Body.Listeners != nil { + for _, listener := range listListenersResp.Body.Listeners { + aliListenerIds = append(aliListenerIds, *listener.ListenerId) + } + } + + if listListenersResp.Body.NextToken == nil { + break + } else { + listListenersToken = listListenersResp.Body.NextToken + listListenersPage += 1 + } + } + + d.infos = append(d.infos, toStr("已查询到 NLB 负载均衡实例下的全部 TCPSSL 监听", aliListenerIds)) + + // 上传证书到 SSL + uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + if err != nil { + return err + } + + d.infos = append(d.infos, toStr("已上传证书", uploadResult)) + + // 批量更新监听证书 + var errs []error + for _, aliListenerId := range aliListenerIds { + if err := d.updateListenerCertificate(ctx, aliListenerId, uploadResult.CertId); err != nil { + errs = append(errs, err) + } + } + if len(errs) > 0 { + return errors.Join(errs...) + } + + return nil +} + +func (d *AliyunNLBDeployer) deployToListener(ctx context.Context) error { + aliListenerId := d.option.DeployConfig.GetConfigAsString("listenerId") + if aliListenerId == "" { + return errors.New("`listenerId` is required") + } + + // 上传证书到 SSL + uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + if err != nil { + return err + } + + d.infos = append(d.infos, toStr("已上传证书", uploadResult)) + + // 更新监听 + if err := d.updateListenerCertificate(ctx, aliListenerId, uploadResult.CertId); err != nil { + return err + } + + return nil +} + +func (d *AliyunNLBDeployer) updateListenerCertificate(ctx context.Context, aliListenerId string, aliCertId string) error { + // 查询监听的属性 + // REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getlistenerattribute + getListenerAttributeReq := &nlb20220430.GetListenerAttributeRequest{ + ListenerId: tea.String(aliListenerId), + } + getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'nlb.GetListenerAttribute': %w", err) + } + + d.infos = append(d.infos, toStr("已查询到 NLB 监听配置", getListenerAttributeResp)) + + // 修改监听的属性 + // REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-updatelistenerattribute + updateListenerAttributeReq := &nlb20220430.UpdateListenerAttributeRequest{ + ListenerId: tea.String(aliListenerId), + CertificateIds: []*string{tea.String(aliCertId)}, + } + updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'nlb.UpdateListenerAttribute': %w", err) + } + + d.infos = append(d.infos, toStr("已更新 NLB 监听配置", updateListenerAttributeResp)) + + return nil +} diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index 17c9091d..37824cab 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -21,9 +21,14 @@ const ( targetAliyunOSS = "aliyun-oss" targetAliyunCDN = "aliyun-cdn" targetAliyunESA = "aliyun-dcdn" + targetAliyunCLB = "aliyun-clb" + targetAliyunALB = "aliyun-alb" + targetAliyunNLB = "aliyun-nlb" targetTencentCDN = "tencent-cdn" + targetTencentECDN = "tencent-ecdn" targetTencentCLB = "tencent-clb" targetTencentCOS = "tencent-cos" + targetTencentTEO = "tencent-teo" targetHuaweiCloudCDN = "huaweicloud-cdn" targetHuaweiCloudELB = "huaweicloud-elb" targetQiniuCdn = "qiniu-cdn" @@ -109,12 +114,22 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep return NewAliyunCDNDeployer(option) case targetAliyunESA: return NewAliyunESADeployer(option) + case targetAliyunCLB: + return NewAliyunCLBDeployer(option) + case targetAliyunALB: + return NewAliyunALBDeployer(option) + case targetAliyunNLB: + return NewAliyunNLBDeployer(option) case targetTencentCDN: - return NewTencentCDNDeployer(option) + return NewTencentCDNDeployer(option) + case targetTencentECDN: + return NewTencentECDNDeployer(option) case targetTencentCLB: return NewTencentCLBDeployer(option) case targetTencentCOS: return NewTencentCOSDeployer(option) + case targetTencentTEO: + return NewTencentTEODeployer(option) case targetHuaweiCloudCDN: return NewHuaweiCloudCDNDeployer(option) case targetHuaweiCloudELB: @@ -130,7 +145,7 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep case targetK8sSecret: return NewK8sSecretDeployer(option) } - return nil, errors.New("not implemented") + return nil, errors.New("unsupported deploy target") } func getProduct(t string) string { diff --git a/internal/deployer/huaweicloud_cdn.go b/internal/deployer/huaweicloud_cdn.go index f7835dcb..ab6e936b 100644 --- a/internal/deployer/huaweicloud_cdn.go +++ b/internal/deployer/huaweicloud_cdn.go @@ -41,9 +41,9 @@ func NewHuaweiCloudCDNDeployer(option *DeployerOption) (Deployer, error) { // TODO: SCM 服务与 DNS 服务所支持的区域可能不一致,这里暂时不传而是使用默认值,仅支持华为云国内版 uploader, err := uploader.NewHuaweiCloudSCMUploader(&uploader.HuaweiCloudSCMUploaderConfig{ - Region: "", AccessKeyId: access.AccessKeyId, SecretAccessKey: access.SecretAccessKey, + Region: "", }) if err != nil { return nil, err diff --git a/internal/deployer/huaweicloud_elb.go b/internal/deployer/huaweicloud_elb.go index e9a6f243..f9f26338 100644 --- a/internal/deployer/huaweicloud_elb.go +++ b/internal/deployer/huaweicloud_elb.go @@ -46,9 +46,9 @@ func NewHuaweiCloudELBDeployer(option *DeployerOption) (Deployer, error) { } uploader, err := uploader.NewHuaweiCloudELBUploader(&uploader.HuaweiCloudELBUploaderConfig{ - Region: option.DeployConfig.GetConfigAsString("region"), AccessKeyId: access.AccessKeyId, SecretAccessKey: access.SecretAccessKey, + Region: option.DeployConfig.GetConfigAsString("region"), }) if err != nil { return nil, err @@ -176,10 +176,15 @@ func (u *HuaweiCloudELBDeployer) getSdkProjectId(accessKeyId, secretAccessKey, r } func (d *HuaweiCloudELBDeployer) deployToCertificate(ctx context.Context) error { + hcCertId := d.option.DeployConfig.GetConfigAsString("certificateId") + if hcCertId == "" { + return errors.New("`certificateId` is required") + } + // 更新证书 // REF: https://support.huaweicloud.com/api-elb/UpdateCertificate.html updateCertificateReq := &hcElbModel.UpdateCertificateRequest{ - CertificateId: d.option.DeployConfig.GetConfigAsString("certificateId"), + CertificateId: hcCertId, Body: &hcElbModel.UpdateCertificateRequestBody{ Certificate: &hcElbModel.UpdateCertificateOption{ Certificate: cast.StringPtr(d.option.Certificate.Certificate), @@ -198,21 +203,27 @@ func (d *HuaweiCloudELBDeployer) deployToCertificate(ctx context.Context) error } func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error { + hcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId") + if hcLoadbalancerId == "" { + return errors.New("`loadbalancerId` is required") + } + + hcListenerIds := make([]string, 0) + // 查询负载均衡器详情 // REF: https://support.huaweicloud.com/api-elb/ShowLoadBalancer.html showLoadBalancerReq := &hcElbModel.ShowLoadBalancerRequest{ - LoadbalancerId: d.option.DeployConfig.GetConfigAsString("loadbalancerId"), + LoadbalancerId: hcLoadbalancerId, } showLoadBalancerResp, err := d.sdkClient.ShowLoadBalancer(showLoadBalancerReq) if err != nil { return fmt.Errorf("failed to execute sdk request 'elb.ShowLoadBalancer': %w", err) } - d.infos = append(d.infos, toStr("已查询到到 ELB 负载均衡器", showLoadBalancerResp)) + d.infos = append(d.infos, toStr("已查询到 ELB 负载均衡器", showLoadBalancerResp)) // 查询监听器列表 // REF: https://support.huaweicloud.com/api-elb/ListListeners.html - listenerIds := make([]string, 0) listListenersLimit := int32(2000) var listListenersMarker *string = nil for { @@ -229,7 +240,7 @@ func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error if listListenersResp.Listeners != nil { for _, listener := range *listListenersResp.Listeners { - listenerIds = append(listenerIds, listener.Id) + hcListenerIds = append(hcListenerIds, listener.Id) } } @@ -240,7 +251,7 @@ func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error } } - d.infos = append(d.infos, toStr("已查询到到 ELB 负载均衡器下的监听器", listenerIds)) + d.infos = append(d.infos, toStr("已查询到 ELB 负载均衡器下的监听器", hcListenerIds)) // 上传证书到 SCM uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) @@ -252,8 +263,8 @@ func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error // 批量更新监听器证书 var errs []error - for _, listenerId := range listenerIds { - if err := d.updateListenerCertificate(ctx, listenerId, uploadResult.CertId); err != nil { + for _, hcListenerId := range hcListenerIds { + if err := d.updateListenerCertificate(ctx, hcListenerId, uploadResult.CertId); err != nil { errs = append(errs, err) } } @@ -265,6 +276,11 @@ func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error } func (d *HuaweiCloudELBDeployer) deployToListener(ctx context.Context) error { + hcListenerId := d.option.DeployConfig.GetConfigAsString("listenerId") + if hcListenerId == "" { + return errors.New("`listenerId` is required") + } + // 上传证书到 SCM uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) if err != nil { @@ -274,7 +290,7 @@ func (d *HuaweiCloudELBDeployer) deployToListener(ctx context.Context) error { d.infos = append(d.infos, toStr("已上传证书", uploadResult)) // 更新监听器证书 - if err := d.updateListenerCertificate(ctx, d.option.DeployConfig.GetConfigAsString("listenerId"), uploadResult.CertId); err != nil { + if err := d.updateListenerCertificate(ctx, hcListenerId, uploadResult.CertId); err != nil { return err } @@ -292,7 +308,7 @@ func (d *HuaweiCloudELBDeployer) updateListenerCertificate(ctx context.Context, return fmt.Errorf("failed to execute sdk request 'elb.ShowListener': %w", err) } - d.infos = append(d.infos, toStr("已查询到到 ELB 监听器", showListenerResp)) + d.infos = append(d.infos, toStr("已查询到 ELB 监听器", showListenerResp)) // 更新监听器 // REF: https://support.huaweicloud.com/api-elb/UpdateListener.html @@ -359,7 +375,7 @@ func (d *HuaweiCloudELBDeployer) updateListenerCertificate(ctx context.Context, return fmt.Errorf("failed to execute sdk request 'elb.UpdateListener': %w", err) } - d.infos = append(d.infos, toStr("已更新监听器", updateListenerResp)) + d.infos = append(d.infos, toStr("已更新 ELB 监听器", updateListenerResp)) return nil } diff --git a/internal/deployer/k8s_secret.go b/internal/deployer/k8s_secret.go index 89789269..dfc56889 100644 --- a/internal/deployer/k8s_secret.go +++ b/internal/deployer/k8s_secret.go @@ -9,6 +9,7 @@ import ( corev1 "k8s.io/api/core/v1" k8sMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "github.com/usual2970/certimate/internal/domain" @@ -118,11 +119,18 @@ func (d *K8sSecretDeployer) Deploy(ctx context.Context) error { } func (d *K8sSecretDeployer) createClient(access *domain.KubernetesAccess) (*kubernetes.Clientset, error) { - kubeConfig, err := clientcmd.NewClientConfigFromBytes([]byte(access.KubeConfig)) - if err != nil { - return nil, err + var config *rest.Config + var err error + if access.KubeConfig == "" { + config, err = rest.InClusterConfig() + } else { + kubeConfig, err := clientcmd.NewClientConfigFromBytes([]byte(access.KubeConfig)) + if err != nil { + return nil, err + } + config, err = kubeConfig.ClientConfig() + } - config, err := kubeConfig.ClientConfig() if err != nil { return nil, err } @@ -131,6 +139,5 @@ func (d *K8sSecretDeployer) createClient(access *domain.KubernetesAccess) (*kube if err != nil { return nil, err } - return client, nil } diff --git a/internal/deployer/tencent_ecdn.go b/internal/deployer/tencent_ecdn.go new file mode 100644 index 00000000..ea52794e --- /dev/null +++ b/internal/deployer/tencent_ecdn.go @@ -0,0 +1,146 @@ +package deployer + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "strings" + + cdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" + ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" + + "github.com/usual2970/certimate/internal/domain" + "github.com/usual2970/certimate/internal/utils/rand" +) + +type TencentECDNDeployer struct { + option *DeployerOption + credential *common.Credential + infos []string +} + +func NewTencentECDNDeployer(option *DeployerOption) (Deployer, error) { + access := &domain.TencentAccess{} + if err := json.Unmarshal([]byte(option.Access), access); err != nil { + return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err) + } + + credential := common.NewCredential( + access.SecretId, + access.SecretKey, + ) + + return &TencentECDNDeployer{ + option: option, + credential: credential, + infos: make([]string, 0), + }, nil +} + +func (d *TencentECDNDeployer) GetID() string { + return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) +} + +func (d *TencentECDNDeployer) GetInfo() []string { + return d.infos +} + +func (d *TencentECDNDeployer) Deploy(ctx context.Context) error { + // 上传证书 + certId, err := d.uploadCert() + if err != nil { + return fmt.Errorf("failed to upload certificate: %w", err) + } + d.infos = append(d.infos, toStr("上传证书", certId)) + + if err := d.deploy(certId); err != nil { + return fmt.Errorf("failed to deploy: %w", err) + } + + return nil +} + +func (d *TencentECDNDeployer) uploadCert() (string, error) { + cpf := profile.NewClientProfile() + cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" + + client, _ := ssl.NewClient(d.credential, "", cpf) + + request := ssl.NewUploadCertificateRequest() + + request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate) + request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey) + request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6)) + request.Repeatable = common.BoolPtr(false) + + response, err := client.UploadCertificate(request) + if err != nil { + return "", fmt.Errorf("failed to upload certificate: %w", err) + } + + return *response.Response.CertificateId, nil +} + +func (d *TencentECDNDeployer) deploy(certId string) error { + cpf := profile.NewClientProfile() + cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" + // 实例化要请求产品的client对象,clientProfile是可选的 + client, _ := ssl.NewClient(d.credential, "", cpf) + + // 实例化一个请求对象,每个接口都会对应一个request对象 + request := ssl.NewDeployCertificateInstanceRequest() + + request.CertificateId = common.StringPtr(certId) + request.ResourceType = common.StringPtr("ecdn") + request.Status = common.Int64Ptr(1) + + // 如果是泛域名就从cdn列表下获取SSL证书中的可用域名 + domain := getDeployString(d.option.DeployConfig, "domain") + if strings.Contains(domain, "*") { + list, errGetList := d.getDomainList() + if errGetList != nil { + return fmt.Errorf("failed to get certificate domain list: %w", errGetList) + } + if list == nil || len(list) == 0 { + return fmt.Errorf("failed to get certificate domain list: empty list.") + } + request.InstanceIdList = common.StringPtrs(list) + } else { // 否则直接使用传入的域名 + request.InstanceIdList = common.StringPtrs([]string{domain}) + } + + // 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应 + resp, err := client.DeployCertificateInstance(request) + if err != nil { + return fmt.Errorf("failed to deploy certificate: %w", err) + } + d.infos = append(d.infos, toStr("部署证书", resp.Response)) + return nil +} + +func (d *TencentECDNDeployer) getDomainList() ([]string, error) { + cpf := profile.NewClientProfile() + cpf.HttpProfile.Endpoint = "cdn.tencentcloudapi.com" + client, _ := cdn.NewClient(d.credential, "", cpf) + + request := cdn.NewDescribeCertDomainsRequest() + + cert := base64.StdEncoding.EncodeToString([]byte(d.option.Certificate.Certificate)) + request.Cert = &cert + request.Product = common.StringPtr("ecdn") + + response, err := client.DescribeCertDomains(request) + if err != nil { + return nil, fmt.Errorf("failed to get domain list: %w", err) + } + + domains := make([]string, 0) + for _, domain := range response.Response.Domains { + domains = append(domains, *domain) + } + + return domains, nil +} diff --git a/internal/deployer/tencent_teo.go b/internal/deployer/tencent_teo.go new file mode 100644 index 00000000..f31aee8d --- /dev/null +++ b/internal/deployer/tencent_teo.go @@ -0,0 +1,111 @@ +package deployer + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + teo "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" + ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" + + "github.com/usual2970/certimate/internal/domain" + "github.com/usual2970/certimate/internal/utils/rand" +) + +type TencentTEODeployer struct { + option *DeployerOption + credential *common.Credential + infos []string +} + +func NewTencentTEODeployer(option *DeployerOption) (Deployer, error) { + access := &domain.TencentAccess{} + if err := json.Unmarshal([]byte(option.Access), access); err != nil { + return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err) + } + + credential := common.NewCredential( + access.SecretId, + access.SecretKey, + ) + + return &TencentTEODeployer{ + option: option, + credential: credential, + infos: make([]string, 0), + }, nil +} + +func (d *TencentTEODeployer) GetID() string { + return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) +} + +func (d *TencentTEODeployer) GetInfo() []string { + return d.infos +} + +func (d *TencentTEODeployer) Deploy(ctx context.Context) error { + // 上传证书 + certId, err := d.uploadCert() + if err != nil { + return fmt.Errorf("failed to upload certificate: %w", err) + } + d.infos = append(d.infos, toStr("上传证书", certId)) + + if err := d.deploy(certId); err != nil { + return fmt.Errorf("failed to deploy: %w", err) + } + + return nil +} + +func (d *TencentTEODeployer) uploadCert() (string, error) { + cpf := profile.NewClientProfile() + cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" + + client, _ := ssl.NewClient(d.credential, "", cpf) + + request := ssl.NewUploadCertificateRequest() + + request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate) + request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey) + request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6)) + request.Repeatable = common.BoolPtr(false) + + response, err := client.UploadCertificate(request) + if err != nil { + return "", fmt.Errorf("failed to upload certificate: %w", err) + } + + return *response.Response.CertificateId, nil +} + +func (d *TencentTEODeployer) deploy(certId string) error { + cpf := profile.NewClientProfile() + cpf.HttpProfile.Endpoint = "teo.tencentcloudapi.com" + // 实例化要请求产品的client对象,clientProfile是可选的 + client, _ := teo.NewClient(d.credential, "", cpf) + + // 实例化一个请求对象,每个接口都会对应一个request对象 + request := teo.NewModifyHostsCertificateRequest() + + request.ZoneId = common.StringPtr(getDeployString(d.option.DeployConfig, "zoneId")) + request.Mode = common.StringPtr("sslcert") + request.ServerCertInfo = []*teo.ServerCertInfo{{ + CertId: common.StringPtr(certId), + }} + + domains := strings.Split(strings.ReplaceAll(d.option.Domain, "\r\n", "\n"),"\n") + request.Hosts = common.StringPtrs(domains) + + // 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应 + resp, err := client.ModifyHostsCertificate(request) + if err != nil { + return fmt.Errorf("failed to deploy certificate: %w", err) + } + d.infos = append(d.infos, toStr("部署证书", resp.Response)) + return nil +} diff --git a/internal/domain/domains.go b/internal/domain/domains.go index 78acbb3d..bed38ac2 100644 --- a/internal/domain/domains.go +++ b/internal/domain/domains.go @@ -18,7 +18,6 @@ type DeployConfig struct { Config map[string]any `json:"config"` } - // 以字符串形式获取配置项。 // // 入参: @@ -52,6 +51,39 @@ func (dc *DeployConfig) GetConfigOrDefaultAsString(key string, defaultValue stri return defaultValue } +// 以 32 位整数形式获取配置项。 +// +// 入参: +// - key: 配置项的键。 +// +// 出参: +// - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回 0。 +func (dc *DeployConfig) GetConfigAsInt32(key string) int32 { + return dc.GetConfigOrDefaultAsInt32(key, 0) +} + +// 以 32 位整数形式获取配置项。 +// +// 入参: +// - key: 配置项的键。 +// - defaultValue: 默认值。 +// +// 出参: +// - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回默认值。 +func (dc *DeployConfig) GetConfigOrDefaultAsInt32(key string, defaultValue int32) int32 { + if dc.Config == nil { + return defaultValue + } + + if value, ok := dc.Config[key]; ok { + if result, ok := value.(int32); ok { + return result + } + } + + return defaultValue +} + // 以布尔形式获取配置项。 // // 入参: diff --git a/internal/domain/notify.go b/internal/domain/notify.go index 11252432..de9cf6cf 100644 --- a/internal/domain/notify.go +++ b/internal/domain/notify.go @@ -5,6 +5,7 @@ const ( NotifyChannelWebhook = "webhook" NotifyChannelTelegram = "telegram" NotifyChannelLark = "lark" + NotifyChannelServerChan = "serverchan" ) type NotifyTestPushReq struct { diff --git a/internal/notify/notify.go b/internal/notify/notify.go index e5265b51..3dfa643b 100644 --- a/internal/notify/notify.go +++ b/internal/notify/notify.go @@ -5,6 +5,8 @@ import ( "fmt" "strconv" + stdhttp "net/http" + "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/utils/app" @@ -102,6 +104,8 @@ func getNotifier(channel string, conf map[string]any) (notifyPackage.Notifier, e return getLarkNotifier(conf), nil case domain.NotifyChannelWebhook: return getWebhookNotifier(conf), nil + case domain.NotifyChannelServerChan: + return getServerChanNotifier(conf), nil } return nil, fmt.Errorf("notifier not found") @@ -132,6 +136,25 @@ func getTelegramNotifier(conf map[string]any) notifyPackage.Notifier { return rs } +func getServerChanNotifier(conf map[string]any) notifyPackage.Notifier { + rs := http.New() + + rs.AddReceivers(&http.Webhook{ + URL: getString(conf, "url"), + Header: stdhttp.Header{}, + ContentType: "application/json", + Method: stdhttp.MethodPost, + BuildPayload: func(subject, message string) (payload any) { + return map[string]string{ + "text": subject, + "desp": message, + } + }, + }) + + return rs +} + func getDingTalkNotifier(conf map[string]any) notifyPackage.Notifier { return dingding.New(&dingding.Config{ Token: getString(conf, "accessToken"), diff --git a/internal/pkg/core/uploader/uploader_aliyun_cas.go b/internal/pkg/core/uploader/uploader_aliyun_cas.go index 64d2e94c..b6a1f792 100644 --- a/internal/pkg/core/uploader/uploader_aliyun_cas.go +++ b/internal/pkg/core/uploader/uploader_aliyun_cas.go @@ -15,9 +15,9 @@ import ( ) type AliyunCASUploaderConfig struct { - Region string `json:"region"` AccessKeyId string `json:"accessKeyId"` AccessKeySecret string `json:"accessKeySecret"` + Region string `json:"region"` } type AliyunCASUploader struct { @@ -28,9 +28,9 @@ type AliyunCASUploader struct { func NewAliyunCASUploader(config *AliyunCASUploaderConfig) (Uploader, error) { client, err := (&AliyunCASUploader{}).createSdkClient( - config.Region, config.AccessKeyId, config.AccessKeySecret, + config.Region, ) if err != nil { return nil, fmt.Errorf("failed to create sdk client: %w", err) @@ -81,12 +81,12 @@ func (u *AliyunCASUploader) Upload(ctx context.Context, certPem string, privkeyP if *getUserCertificateDetailResp.Body.Cert == certPem { isSameCert = true } else { - cert, err := x509.ParseCertificateFromPEM(*getUserCertificateDetailResp.Body.Cert) + oldCertX509, err := x509.ParseCertificateFromPEM(*getUserCertificateDetailResp.Body.Cert) if err != nil { continue } - isSameCert = x509.EqualCertificate(certX509, cert) + isSameCert = x509.EqualCertificate(certX509, oldCertX509) } // 如果已存在相同证书,直接返回已有的证书信息 @@ -133,7 +133,7 @@ func (u *AliyunCASUploader) Upload(ctx context.Context, certPem string, privkeyP }, nil } -func (u *AliyunCASUploader) createSdkClient(region, accessKeyId, accessKeySecret string) (*cas20200407.Client, error) { +func (u *AliyunCASUploader) createSdkClient(accessKeyId, accessKeySecret, region string) (*cas20200407.Client, error) { if region == "" { region = "cn-hangzhou" // CAS 服务默认区域:华东一杭州 } @@ -147,10 +147,6 @@ func (u *AliyunCASUploader) createSdkClient(region, accessKeyId, accessKeySecret switch region { case "cn-hangzhou": endpoint = "cas.aliyuncs.com" - case "ap-southeast-1": - endpoint = "cas.ap-southeast-1.aliyuncs.com" - case "eu-central-1": - endpoint = "cas.eu-central-1.aliyuncs.com" default: endpoint = fmt.Sprintf("cas.%s.aliyuncs.com", region) } diff --git a/internal/pkg/core/uploader/uploader_aliyun_slb.go b/internal/pkg/core/uploader/uploader_aliyun_slb.go new file mode 100644 index 00000000..99f3c484 --- /dev/null +++ b/internal/pkg/core/uploader/uploader_aliyun_slb.go @@ -0,0 +1,134 @@ +package uploader + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "strings" + "time" + + openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" + slb20140515 "github.com/alibabacloud-go/slb-20140515/v4/client" + util "github.com/alibabacloud-go/tea-utils/v2/service" + "github.com/alibabacloud-go/tea/tea" + + "github.com/usual2970/certimate/internal/pkg/utils/x509" +) + +type AliyunSLBUploaderConfig struct { + AccessKeyId string `json:"accessKeyId"` + AccessKeySecret string `json:"accessKeySecret"` + Region string `json:"region"` +} + +type AliyunSLBUploader struct { + config *AliyunSLBUploaderConfig + sdkClient *slb20140515.Client + sdkRuntime *util.RuntimeOptions +} + +func NewAliyunSLBUploader(config *AliyunSLBUploaderConfig) (Uploader, error) { + client, err := (&AliyunSLBUploader{}).createSdkClient( + config.AccessKeyId, + config.AccessKeySecret, + config.Region, + ) + if err != nil { + return nil, fmt.Errorf("failed to create sdk client: %w", err) + } + + return &AliyunSLBUploader{ + config: config, + sdkClient: client, + sdkRuntime: &util.RuntimeOptions{}, + }, nil +} + +func (u *AliyunSLBUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *UploadResult, err error) { + // 解析证书内容 + certX509, err := x509.ParseCertificateFromPEM(certPem) + if err != nil { + return nil, err + } + + // 查询证书列表,避免重复上传 + // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeservercertificates + describeServerCertificatesReq := &slb20140515.DescribeServerCertificatesRequest{ + RegionId: tea.String(u.config.Region), + } + describeServerCertificatesResp, err := u.sdkClient.DescribeServerCertificatesWithOptions(describeServerCertificatesReq, u.sdkRuntime) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'slb.DescribeServerCertificates': %w", err) + } + + if describeServerCertificatesResp.Body.ServerCertificates != nil && describeServerCertificatesResp.Body.ServerCertificates.ServerCertificate != nil { + fingerprint := sha256.Sum256(certX509.Raw) + fingerprintHex := hex.EncodeToString(fingerprint[:]) + for _, certDetail := range describeServerCertificatesResp.Body.ServerCertificates.ServerCertificate { + isSameCert := *certDetail.IsAliCloudCertificate == 0 && + strings.EqualFold(fingerprintHex, strings.ReplaceAll(*certDetail.Fingerprint, ":", "")) && + strings.EqualFold(certX509.Subject.CommonName, *certDetail.CommonName) + // 如果已存在相同证书,直接返回已有的证书信息 + if isSameCert { + return &UploadResult{ + CertId: *certDetail.ServerCertificateId, + CertName: *certDetail.ServerCertificateName, + }, nil + } + } + } + + // 生成新证书名(需符合阿里云命名规则) + var certId, certName string + certName = fmt.Sprintf("certimate_%d", time.Now().UnixMilli()) + + // 上传新证书 + // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-uploadservercertificate + uploadServerCertificateReq := &slb20140515.UploadServerCertificateRequest{ + RegionId: tea.String(u.config.Region), + ServerCertificateName: tea.String(certName), + ServerCertificate: tea.String(certPem), + PrivateKey: tea.String(privkeyPem), + } + uploadServerCertificateResp, err := u.sdkClient.UploadServerCertificateWithOptions(uploadServerCertificateReq, u.sdkRuntime) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'slb.UploadServerCertificate': %w", err) + } + + certId = *uploadServerCertificateResp.Body.ServerCertificateId + return &UploadResult{ + CertId: certId, + CertName: certName, + }, nil +} + +func (u *AliyunSLBUploader) createSdkClient(accessKeyId, accessKeySecret, region string) (*slb20140515.Client, error) { + if region == "" { + region = "cn-hangzhou" // SLB 服务默认区域:华东一杭州 + } + + aConfig := &openapi.Config{ + AccessKeyId: tea.String(accessKeyId), + AccessKeySecret: tea.String(accessKeySecret), + } + + var endpoint string + switch region { + case "cn-hangzhou": + case "cn-hangzhou-finance": + case "cn-shanghai-finance-1": + case "cn-shenzhen-finance-1": + endpoint = "slb.aliyuncs.com" + default: + endpoint = fmt.Sprintf("slb.%s.aliyuncs.com", region) + } + aConfig.Endpoint = tea.String(endpoint) + + client, err := slb20140515.NewClient(aConfig) + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/internal/pkg/utils/x509/x509.go b/internal/pkg/utils/x509/x509.go index 0239df69..09d67d3a 100644 --- a/internal/pkg/utils/x509/x509.go +++ b/internal/pkg/utils/x509/x509.go @@ -7,6 +7,23 @@ import ( "fmt" ) +// 比较两个 x509.Certificate 对象,判断它们是否是同一张证书。 +// 注意,这不是精确比较,而只是基于证书序列号和数字签名的快速判断,但对于权威 CA 签发的证书来说不会存在误判。 +// +// 入参: +// - a: 待比较的第一个 x509.Certificate 对象。 +// - b: 待比较的第二个 x509.Certificate 对象。 +// +// 出参: +// - 是否相同。 +func EqualCertificate(a, b *x509.Certificate) bool { + return string(a.Signature) == string(b.Signature) && + a.SignatureAlgorithm == b.SignatureAlgorithm && + a.SerialNumber.String() == b.SerialNumber.String() && + a.Issuer.SerialNumber == b.Issuer.SerialNumber && + a.Subject.SerialNumber == b.Subject.SerialNumber +} + // 从 PEM 编码的证书字符串解析并返回一个 x509.Certificate 对象。 // // 入参: @@ -31,26 +48,40 @@ func ParseCertificateFromPEM(certPem string) (cert *x509.Certificate, err error) return cert, nil } -// 比较两个 x509.Certificate 对象,判断它们是否是同一张证书。 -// 注意,这不是精确比较,而只是基于证书序列号和数字签名的快速判断,但对于权威 CA 签发的证书来说不会存在误判。 +// 从 PEM 编码的私钥字符串解析并返回一个 ECDSA 私钥对象。 // // 入参: -// - a: 待比较的第一个 x509.Certificate 对象。 -// - b: 待比较的第二个 x509.Certificate 对象。 +// - privkeyPem: 私钥 PEM 内容。 // // 出参: -// - 是否相同。 -func EqualCertificate(a, b *x509.Certificate) bool { - return string(a.Signature) == string(b.Signature) && - a.SignatureAlgorithm == b.SignatureAlgorithm && - a.SerialNumber.String() == b.SerialNumber.String() && - a.Issuer.SerialNumber == b.Issuer.SerialNumber && - a.Subject.SerialNumber == b.Subject.SerialNumber +// - privkey: ecdsa.PrivateKey 对象。 +// - err: 错误。 +func ParseECPrivateKeyFromPEM(privkeyPem string) (privkey *ecdsa.PrivateKey, err error) { + pemData := []byte(privkeyPem) + + block, _ := pem.Decode(pemData) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM block") + } + + privkey, err = x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse private key: %w", err) + } + + return privkey, nil } -// 将 ECDSA 私钥转换为 PEM 格式的字符串。 -func PrivateKeyToPEM(privateKey *ecdsa.PrivateKey) (string, error) { - data, err := x509.MarshalECPrivateKey(privateKey) +// 将 ECDSA 私钥转换为 PEM 编码的字符串。 +// +// 入参: +// - privkey: ecdsa.PrivateKey 对象。 +// +// 出参: +// - privkeyPem: 私钥 PEM 内容。 +// - err: 错误。 +func ConvertECPrivateKeyToPEM(privkey *ecdsa.PrivateKey) (privkeyPem string, err error) { + data, err := x509.MarshalECPrivateKey(privkey) if err != nil { return "", fmt.Errorf("failed to marshal EC private key: %w", err) } @@ -62,20 +93,3 @@ func PrivateKeyToPEM(privateKey *ecdsa.PrivateKey) (string, error) { return string(pem.EncodeToMemory(block)), nil } - -// 从 PEM 编码的私钥字符串解析并返回一个 ECDSA 私钥对象。 -func ParsePrivateKeyFromPEM(privateKeyPem string) (*ecdsa.PrivateKey, error) { - pemData := []byte(privateKeyPem) - - block, _ := pem.Decode(pemData) - if block == nil { - return nil, fmt.Errorf("failed to decode PEM block") - } - - privateKey, err := x509.ParseECPrivateKey(block.Bytes) - if err != nil { - return nil, fmt.Errorf("failed to parse private key: %w", err) - } - - return privateKey, nil -} diff --git a/ui/src/components/certimate/AccessKubernetesForm.tsx b/ui/src/components/certimate/AccessKubernetesForm.tsx index bb84c89c..23a696a9 100644 --- a/ui/src/components/certimate/AccessKubernetesForm.tsx +++ b/ui/src/components/certimate/AccessKubernetesForm.tsx @@ -37,7 +37,7 @@ const AccessKubernetesForm = ({ data, op, onAfterReq }: AccessKubernetesFormProp configType: accessTypeFormSchema, kubeConfig: z .string() - .min(1, "access.authorization.form.k8s_kubeconfig.placeholder") + .min(0, "access.authorization.form.k8s_kubeconfig.placeholder") .max(20480, t("common.errmsg.string_max", { max: 20480 })), kubeConfigFile: z.any().optional(), }); @@ -191,3 +191,4 @@ const AccessKubernetesForm = ({ data, op, onAfterReq }: AccessKubernetesFormProp }; export default AccessKubernetesForm; + diff --git a/ui/src/components/certimate/DeployEditDialog.tsx b/ui/src/components/certimate/DeployEditDialog.tsx index 24d122c7..3e4804b9 100644 --- a/ui/src/components/certimate/DeployEditDialog.tsx +++ b/ui/src/components/certimate/DeployEditDialog.tsx @@ -11,9 +11,13 @@ import AccessEditDialog from "./AccessEditDialog"; import { Context as DeployEditContext } from "./DeployEdit"; import DeployToAliyunOSS from "./DeployToAliyunOSS"; import DeployToAliyunCDN from "./DeployToAliyunCDN"; +import DeployToAliyunCLB from "./DeployToAliyunCLB"; +import DeployToAliyunALB from "./DeployToAliyunALB"; +import DeployToAliyunNLB from "./DeployToAliyunNLB"; import DeployToTencentCDN from "./DeployToTencentCDN"; import DeployToTencentCLB from "./DeployToTencentCLB"; import DeployToTencentCOS from "./DeployToTencentCOS"; +import DeployToTencentTEO from "./DeployToTencentTEO"; import DeployToHuaweiCloudCDN from "./DeployToHuaweiCloudCDN"; import DeployToHuaweiCloudELB from "./DeployToHuaweiCloudELB"; import DeployToQiniuCDN from "./DeployToQiniuCDN"; @@ -119,7 +123,17 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro case "aliyun-dcdn": childComponent = ; break; + case "aliyun-clb": + childComponent = ; + break; + case "aliyun-alb": + childComponent = ; + break; + case "aliyun-nlb": + childComponent = ; + break; case "tencent-cdn": + case "tencent-ecdn": childComponent = ; break; case "tencent-clb": @@ -128,6 +142,9 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro case "tencent-cos": childComponent = ; break; + case "tencent-teo": + childComponent = ; + break; case "huaweicloud-cdn": childComponent = ; break; diff --git a/ui/src/components/certimate/DeployToAliyunALB.tsx b/ui/src/components/certimate/DeployToAliyunALB.tsx new file mode 100644 index 00000000..cf7feba9 --- /dev/null +++ b/ui/src/components/certimate/DeployToAliyunALB.tsx @@ -0,0 +1,162 @@ +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { z } from "zod"; +import { produce } from "immer"; + +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { useDeployEditContext } from "./DeployEdit"; + +const DeployToAliyunALB = () => { + const { t } = useTranslation(); + + const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + + useEffect(() => { + if (!data.id) { + setDeploy({ + ...data, + config: { + region: "cn-hangzhou", + resourceType: "", + loadbalancerId: "", + listenerId: "", + }, + }); + } + }, []); + + useEffect(() => { + setError({}); + }, []); + + const formSchema = z + .object({ + region: z.string().min(1, t("domain.deployment.form.aliyun_alb_region.placeholder")), + resourceType: z.union([z.literal("loadbalancer"), z.literal("listener")], { + message: t("domain.deployment.form.aliyun_alb_resource_type.placeholder"), + }), + loadbalancerId: z.string().optional(), + listenerId: z.string().optional(), + }) + .refine((data) => (data.resourceType === "loadbalancer" ? !!data.loadbalancerId?.trim() : true), { + message: t("domain.deployment.form.aliyun_alb_loadbalancer_id.placeholder"), + path: ["loadbalancerId"], + }) + .refine((data) => (data.resourceType === "listener" ? !!data.listenerId?.trim() : true), { + message: t("domain.deployment.form.aliyun_alb_listener_id.placeholder"), + path: ["listenerId"], + }); + + useEffect(() => { + const res = formSchema.safeParse(data.config); + if (!res.success) { + setError({ + ...error, + region: res.error.errors.find((e) => e.path[0] === "region")?.message, + resourceType: res.error.errors.find((e) => e.path[0] === "resourceType")?.message, + loadbalancerId: res.error.errors.find((e) => e.path[0] === "loadbalancerId")?.message, + listenerId: res.error.errors.find((e) => e.path[0] === "listenerId")?.message, + }); + } else { + setError({ + ...error, + region: undefined, + resourceType: undefined, + loadbalancerId: undefined, + listenerId: undefined, + }); + } + }, [data]); + + return ( +
+
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.region = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.region}
+
+ +
+ + +
{error?.resourceType}
+
+ + {data?.config?.resourceType === "loadbalancer" ? ( +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.loadbalancerId = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.loadbalancerId}
+
+ ) : ( + <> + )} + + {data?.config?.resourceType === "listener" ? ( +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.listenerId = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.listenerId}
+
+ ) : ( + <> + )} +
+ ); +}; + +export default DeployToAliyunALB; diff --git a/ui/src/components/certimate/DeployToAliyunCLB.tsx b/ui/src/components/certimate/DeployToAliyunCLB.tsx new file mode 100644 index 00000000..eb41c0ac --- /dev/null +++ b/ui/src/components/certimate/DeployToAliyunCLB.tsx @@ -0,0 +1,158 @@ +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { z } from "zod"; +import { produce } from "immer"; + +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { useDeployEditContext } from "./DeployEdit"; + +const DeployToAliyunCLB = () => { + const { t } = useTranslation(); + + const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + + useEffect(() => { + if (!data.id) { + setDeploy({ + ...data, + config: { + region: "cn-hangzhou", + resourceType: "", + loadbalancerId: "", + listenerPort: "443", + }, + }); + } + }, []); + + useEffect(() => { + setError({}); + }, []); + + const formSchema = z + .object({ + region: z.string().min(1, t("domain.deployment.form.aliyun_clb_region.placeholder")), + resourceType: z.union([z.literal("certificate"), z.literal("loadbalancer"), z.literal("listener")], { + message: t("domain.deployment.form.aliyun_clb_resource_type.placeholder"), + }), + loadbalancerId: z.string().optional(), + listenerPort: z.string().optional(), + }) + .refine((data) => (data.resourceType === "loadbalancer" || data.resourceType === "listener" ? !!data.loadbalancerId?.trim() : true), { + message: t("domain.deployment.form.aliyun_clb_loadbalancer_id.placeholder"), + path: ["loadbalancerId"], + }) + .refine((data) => (data.resourceType === "listener" ? +data.listenerPort! > 0 && +data.listenerPort! < 65535 : true), { + message: t("domain.deployment.form.aliyun_clb_listener_port.placeholder"), + path: ["listenerPort"], + }); + + useEffect(() => { + const res = formSchema.safeParse(data.config); + if (!res.success) { + setError({ + ...error, + region: res.error.errors.find((e) => e.path[0] === "region")?.message, + resourceType: res.error.errors.find((e) => e.path[0] === "resourceType")?.message, + loadbalancerId: res.error.errors.find((e) => e.path[0] === "loadbalancerId")?.message, + listenerPort: res.error.errors.find((e) => e.path[0] === "listenerPort")?.message, + }); + } else { + setError({ + ...error, + region: undefined, + resourceType: undefined, + loadbalancerId: undefined, + listenerPort: undefined, + }); + } + }, [data]); + + return ( +
+
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.region = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.region}
+
+ +
+ + +
{error?.resourceType}
+
+ +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.loadbalancerId = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.loadbalancerId}
+
+ + {data?.config?.resourceType === "listener" ? ( +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.listenerPort = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.listenerPort}
+
+ ) : ( + <> + )} +
+ ); +}; + +export default DeployToAliyunCLB; diff --git a/ui/src/components/certimate/DeployToAliyunNLB.tsx b/ui/src/components/certimate/DeployToAliyunNLB.tsx new file mode 100644 index 00000000..38d6b1f7 --- /dev/null +++ b/ui/src/components/certimate/DeployToAliyunNLB.tsx @@ -0,0 +1,162 @@ +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { z } from "zod"; +import { produce } from "immer"; + +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { useDeployEditContext } from "./DeployEdit"; + +const DeployToAliyunNLB = () => { + const { t } = useTranslation(); + + const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + + useEffect(() => { + if (!data.id) { + setDeploy({ + ...data, + config: { + region: "cn-hangzhou", + resourceType: "", + loadbalancerId: "", + listenerId: "", + }, + }); + } + }, []); + + useEffect(() => { + setError({}); + }, []); + + const formSchema = z + .object({ + region: z.string().min(1, t("domain.deployment.form.aliyun_nlb_region.placeholder")), + resourceType: z.union([z.literal("loadbalancer"), z.literal("listener")], { + message: t("domain.deployment.form.aliyun_nlb_resource_type.placeholder"), + }), + loadbalancerId: z.string().optional(), + listenerId: z.string().optional(), + }) + .refine((data) => (data.resourceType === "loadbalancer" ? !!data.loadbalancerId?.trim() : true), { + message: t("domain.deployment.form.aliyun_nlb_loadbalancer_id.placeholder"), + path: ["loadbalancerId"], + }) + .refine((data) => (data.resourceType === "listener" ? !!data.listenerId?.trim() : true), { + message: t("domain.deployment.form.aliyun_nlb_listener_id.placeholder"), + path: ["listenerId"], + }); + + useEffect(() => { + const res = formSchema.safeParse(data.config); + if (!res.success) { + setError({ + ...error, + region: res.error.errors.find((e) => e.path[0] === "region")?.message, + resourceType: res.error.errors.find((e) => e.path[0] === "resourceType")?.message, + loadbalancerId: res.error.errors.find((e) => e.path[0] === "loadbalancerId")?.message, + listenerId: res.error.errors.find((e) => e.path[0] === "listenerId")?.message, + }); + } else { + setError({ + ...error, + region: undefined, + resourceType: undefined, + loadbalancerId: undefined, + listenerId: undefined, + }); + } + }, [data]); + + return ( +
+
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.region = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.region}
+
+ +
+ + +
{error?.resourceType}
+
+ + {data?.config?.resourceType === "loadbalancer" ? ( +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.loadbalancerId = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.loadbalancerId}
+
+ ) : ( + <> + )} + + {data?.config?.resourceType === "listener" ? ( +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.listenerId = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.listenerId}
+
+ ) : ( + <> + )} +
+ ); +}; + +export default DeployToAliyunNLB; diff --git a/ui/src/components/certimate/DeployToHuaweiCloudELB.tsx b/ui/src/components/certimate/DeployToHuaweiCloudELB.tsx index 064df882..e7a7fc4a 100644 --- a/ui/src/components/certimate/DeployToHuaweiCloudELB.tsx +++ b/ui/src/components/certimate/DeployToHuaweiCloudELB.tsx @@ -44,7 +44,7 @@ const DeployToHuaweiCloudCDN = () => { message: t("domain.deployment.form.huaweicloud_elb_certificate_id.placeholder"), path: ["certificateId"], }) - .refine((data) => (data.resourceType === "loadbalancer" ? !!data.certificateId?.trim() : true), { + .refine((data) => (data.resourceType === "loadbalancer" ? !!data.loadbalancerId?.trim() : true), { message: t("domain.deployment.form.huaweicloud_elb_loadbalancer_id.placeholder"), path: ["loadbalancerId"], }) diff --git a/ui/src/components/certimate/DeployToTencentTEO.tsx b/ui/src/components/certimate/DeployToTencentTEO.tsx new file mode 100644 index 00000000..80715fd1 --- /dev/null +++ b/ui/src/components/certimate/DeployToTencentTEO.tsx @@ -0,0 +1,131 @@ +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { z } from "zod"; +import { produce } from "immer"; + +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { useDeployEditContext } from "./DeployEdit"; + +const DeployToTencentTEO = () => { + const { t } = useTranslation(); + + const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + + useEffect(() => { + setError({}); + }, []); + + useEffect(() => { + const resp = domainSchema.safeParse(data.config?.domain); + if (!resp.success) { + setError({ + ...error, + domain: JSON.parse(resp.error.message)[0].message, + }); + } else { + setError({ + ...error, + domain: "", + }); + } + }, [data]); + + useEffect(() => { + const resp = zoneIdSchema.safeParse(data.config?.zoneId); + if (!resp.success) { + setError({ + ...error, + zoneId: JSON.parse(resp.error.message)[0].message, + }); + } else { + setError({ + ...error, + zoneId: "", + }); + } + }, [data]); + + const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { + message: t("common.errmsg.domain_invalid"), + }); + + const zoneIdSchema = z.string().regex(/^zone-[0-9a-zA-Z]{9}$/, { + message: t("common.errmsg.zoneid_invalid"), + }); + + return ( +
+
+ + { + const temp = e.target.value; + + const resp = zoneIdSchema.safeParse(temp); + if (!resp.success) { + setError({ + ...error, + zoneId: JSON.parse(resp.error.message)[0].message, + }); + } else { + setError({ + ...error, + zoneId: "", + }); + } + + const newData = produce(data, (draft) => { + if (!draft.config) { + draft.config = {}; + } + draft.config.zoneId = temp; + }); + setDeploy(newData); + }} + /> +
{error?.zoneId}
+
+ +
+ +