mirror of
https://github.com/certimate-go/certimate.git
synced 2026-09-24 23:10:13 +08:00
refactor(provider): re-implement deployment provider of huaweicloud obs with custom sdk
This commit is contained in:
@@ -17,11 +17,12 @@ func init() {
|
||||
}
|
||||
|
||||
provider, err := dplyimpl.NewDeployer(&dplyimpl.DeployerConfig{
|
||||
AccessKeyId: credentials.AccessKeyId,
|
||||
SecretAccessKey: credentials.SecretAccessKey,
|
||||
Region: xmaps.GetString(options.ProviderExtendedConfig, "region"),
|
||||
Bucket: xmaps.GetString(options.ProviderExtendedConfig, "bucket"),
|
||||
Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"),
|
||||
AccessKeyId: credentials.AccessKeyId,
|
||||
SecretAccessKey: credentials.SecretAccessKey,
|
||||
EnterpriseProjectId: credentials.EnterpriseProjectId,
|
||||
Region: xmaps.GetString(options.ProviderExtendedConfig, "region"),
|
||||
Bucket: xmaps.GetString(options.ProviderExtendedConfig, "bucket"),
|
||||
Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"),
|
||||
})
|
||||
return provider, err
|
||||
})
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
package huaweicloudobs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/certimate-go/certimate/pkg/core"
|
||||
cmgrimpl "github.com/certimate-go/certimate/pkg/core/certmgr/providers/huaweicloud-scm"
|
||||
obssdk "github.com/certimate-go/certimate/pkg/sdk3rd/huaweicloud/obs"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -25,6 +20,8 @@ type DeployerConfig struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// 华为云 SecretAccessKey。
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
// 华为云企业项目 ID。
|
||||
EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"`
|
||||
// 华为云区域。
|
||||
Region string `json:"region"`
|
||||
// 存储桶名。
|
||||
@@ -34,8 +31,10 @@ type DeployerConfig struct {
|
||||
}
|
||||
|
||||
type Deployer struct {
|
||||
config *DeployerConfig
|
||||
logger *slog.Logger
|
||||
config *DeployerConfig
|
||||
logger *slog.Logger
|
||||
sdkClient *obssdk.Client
|
||||
sdkCertmgr core.Certmgr
|
||||
}
|
||||
|
||||
var _ Provider = (*Deployer)(nil)
|
||||
@@ -45,9 +44,25 @@ func NewDeployer(config *DeployerConfig) (*Deployer, error) {
|
||||
return nil, fmt.Errorf("the configuration of the deployer provider is nil")
|
||||
}
|
||||
|
||||
client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region, config.Bucket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create client: %w", err)
|
||||
}
|
||||
|
||||
pcertmgr, err := cmgrimpl.NewCertmgr(&cmgrimpl.CertmgrConfig{
|
||||
AccessKeyId: config.AccessKeyId,
|
||||
SecretAccessKey: config.SecretAccessKey,
|
||||
EnterpriseProjectId: config.EnterpriseProjectId,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create certmgr: %w", err)
|
||||
}
|
||||
|
||||
return &Deployer{
|
||||
config: config,
|
||||
logger: slog.Default(),
|
||||
config: config,
|
||||
logger: slog.Default(),
|
||||
sdkClient: client,
|
||||
sdkCertmgr: pcertmgr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -57,6 +72,8 @@ func (d *Deployer) SetLogger(logger *slog.Logger) {
|
||||
} else {
|
||||
d.logger = logger
|
||||
}
|
||||
|
||||
d.sdkCertmgr.SetLogger(logger)
|
||||
}
|
||||
|
||||
func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*DeployResult, error) {
|
||||
@@ -70,61 +87,42 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep
|
||||
return nil, fmt.Errorf("config `domain` is required")
|
||||
}
|
||||
|
||||
// 上传证书
|
||||
upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
|
||||
} else {
|
||||
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
|
||||
}
|
||||
|
||||
// REF: https://support.huaweicloud.com/usermanual-obs/obs_06_3200.html
|
||||
// REF: https://support.huaweicloud.com/api-obs/obs_04_0059.html
|
||||
url := fmt.Sprintf("https://%s.obs.%s.myhuaweicloud.com/?customdomain=%s", d.config.Bucket, d.config.Region, d.config.Domain)
|
||||
bodyXML := fmt.Sprintf(`
|
||||
<CustomDomainConfiguration>
|
||||
<Name>%s</Name>
|
||||
<Certificate>%s</Certificate>
|
||||
<CertificateChain>%s</CertificateChain>
|
||||
<PrivateKey>%s</PrivateKey>
|
||||
</CustomDomainConfiguration>`,
|
||||
d.config.Bucket+"_"+d.config.Domain, certPEM, certPEM, privkeyPEM,
|
||||
)
|
||||
|
||||
// 计算 Content-MD5(Base64 编码)
|
||||
md5sum := md5.Sum([]byte(bodyXML))
|
||||
md5sumEncoded := base64.StdEncoding.EncodeToString(md5sum[:])
|
||||
|
||||
// 构造签名字符串
|
||||
date := time.Now().UTC().Format(http.TimeFormat)
|
||||
method := "PUT"
|
||||
contentType := "application/xml"
|
||||
canonicalizedResource := fmt.Sprintf("/%s/?customdomain=%s", d.config.Bucket, d.config.Domain)
|
||||
stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s\n%s", method, md5sumEncoded, contentType, date, canonicalizedResource)
|
||||
|
||||
// HMAC-SHA1 签名
|
||||
h := hmac.New(sha1.New, []byte(d.config.SecretAccessKey))
|
||||
h.Write([]byte(stringToSign))
|
||||
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
|
||||
// Authorization
|
||||
authHeader := fmt.Sprintf("OBS %s:%s", d.config.AccessKeyId, signature)
|
||||
|
||||
// 创建请求
|
||||
req, err := http.NewRequest(method, url, bytes.NewBuffer([]byte(bodyXML)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("huaweicloud obs api error: %w", err)
|
||||
putBucketCustomDomainReq := &obssdk.PutBucketCustomDomainRequest{
|
||||
CustomDomain: d.config.Domain,
|
||||
Name: upres.CertName,
|
||||
CertificateId: upres.CertId,
|
||||
Certificate: certPEM,
|
||||
CertificateChain: certPEM,
|
||||
PrivateKey: privkeyPEM,
|
||||
}
|
||||
req.Header.Set("Date", date)
|
||||
req.Header.Set("Authorization", authHeader)
|
||||
req.Header.Set("Content-MD5", md5sumEncoded)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
|
||||
// 请求
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
putBucketCustomDomainResp, err := d.sdkClient.PutBucketCustomDomainWithContext(ctx, d.config.Bucket, putBucketCustomDomainReq)
|
||||
d.logger.Debug("sdk request 'obs.PutBucketCustomDomain'", slog.String("params.bucket", d.config.Bucket), slog.String("params.customdomain", d.config.Domain), slog.Any("request", putBucketCustomDomainReq), slog.Any("response", putBucketCustomDomainResp))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("huaweicloud obs api error: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 响应
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body := &bytes.Buffer{}
|
||||
body.ReadFrom(resp.Body)
|
||||
return nil, fmt.Errorf("huaweicloud obs api error: unexpected status code: %d (resp: %s)", resp.StatusCode, body.String())
|
||||
return nil, fmt.Errorf("failed to execute sdk request 'obs.PutBucketCustomDomain': %w", err)
|
||||
}
|
||||
|
||||
return &DeployResult{}, nil
|
||||
}
|
||||
|
||||
func createSDKClient(accessKeyId, secretAccessKey, region, bucket string) (*obssdk.Client, error) {
|
||||
client, err := obssdk.NewClient("",
|
||||
obssdk.WithAkSk(accessKeyId, secretAccessKey),
|
||||
obssdk.WithRegion(region),
|
||||
obssdk.WithBucket(bucket),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package obs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type PutBucketCustomDomainRequest struct {
|
||||
XMLName xml.Name `json:"-" xml:"CustomDomainConfiguration"`
|
||||
CustomDomain string `json:"-" xml:"-"`
|
||||
Name string `json:"Name,omitempty" xml:"Name,omitempty"`
|
||||
CertificateId string `json:"CertificateId,omitempty" xml:"CertificateId,omitempty"`
|
||||
Certificate string `json:"Certificate,omitempty" xml:"Certificate,omitempty"`
|
||||
CertificateChain string `json:"CertificateChain,omitempty" xml:"CertificateChain,omitempty"`
|
||||
PrivateKey string `json:"PrivateKey,omitempty" xml:"PrivateKey,omitempty"`
|
||||
}
|
||||
|
||||
type PutBucketCustomDomainResponse struct {
|
||||
sdkResponseBase
|
||||
}
|
||||
|
||||
func (c *Client) PutBucketCustomDomain(bucket string, req *PutBucketCustomDomainRequest) (*PutBucketCustomDomainResponse, error) {
|
||||
return c.PutBucketCustomDomainWithContext(context.Background(), bucket, req)
|
||||
}
|
||||
|
||||
func (c *Client) PutBucketCustomDomainWithContext(ctx context.Context, bucket string, req *PutBucketCustomDomainRequest) (*PutBucketCustomDomainResponse, error) {
|
||||
if bucket == "" {
|
||||
return nil, fmt.Errorf("sdkerr: bad request: unset bucket")
|
||||
}
|
||||
if req.CustomDomain == "" {
|
||||
return nil, fmt.Errorf("sdkerr: bad request: unset customdomain")
|
||||
}
|
||||
|
||||
httpreq, err := c.newRequest(http.MethodPut, "/?customdomain="+url.QueryEscape(req.CustomDomain), req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
httpreq.SetContext(ctx)
|
||||
}
|
||||
|
||||
result := &PutBucketCustomDomainResponse{}
|
||||
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package obs
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
|
||||
"github.com/certimate-go/certimate/internal/app"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
rc *resty.Client
|
||||
}
|
||||
|
||||
func NewClient(endpoint string, optFns ...OptionsFunc) (*Client, error) {
|
||||
opts := &Options{}
|
||||
for _, fn := range optFns {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
if opts.Region == "" {
|
||||
return nil, fmt.Errorf("sdkerr: unset region")
|
||||
}
|
||||
if opts.AccessKeyId == "" {
|
||||
return nil, fmt.Errorf("sdkerr: unset accessKeyId")
|
||||
}
|
||||
if opts.SecretAccessKey == "" {
|
||||
return nil, fmt.Errorf("sdkerr: unset secretAccessKey")
|
||||
}
|
||||
|
||||
if endpoint == "" {
|
||||
if opts.Bucket == "" {
|
||||
endpoint = fmt.Sprintf("https://obs.%s.myhuaweicloud.com", url.PathEscape(opts.Region))
|
||||
} else {
|
||||
endpoint = fmt.Sprintf("https://%s.obs.%s.myhuaweicloud.com", url.PathEscape(opts.Bucket), url.PathEscape(opts.Region))
|
||||
}
|
||||
} else {
|
||||
if baseUrl, err := url.Parse(endpoint); err != nil {
|
||||
return nil, fmt.Errorf("sdkerr: invalid endpoint: %w", err)
|
||||
} else if baseUrl.Scheme == "" {
|
||||
endpoint = "https://" + endpoint
|
||||
}
|
||||
}
|
||||
|
||||
restyClient := resty.New().
|
||||
SetBaseURL(endpoint).
|
||||
SetHeader("User-Agent", app.AppUserAgent).
|
||||
SetPreRequestHook(func(c *resty.Client, req *http.Request) error {
|
||||
// API 签名机制:
|
||||
// https://support.huaweicloud.com/api-obs/obs_04_0010.html
|
||||
|
||||
canonicalizedHeaders := ""
|
||||
if len(req.Header) > 0 {
|
||||
keys := make([]string, 0, len(req.Header))
|
||||
for key := range req.Header {
|
||||
key = strings.ToLower(key)
|
||||
if strings.HasPrefix(key, "x-obs-") {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, key := range keys {
|
||||
value := strings.TrimSpace(req.Header.Get(key))
|
||||
canonicalizedHeaders += fmt.Sprintf("%s:%s", key, url.QueryEscape(value))
|
||||
canonicalizedHeaders += "\n"
|
||||
}
|
||||
}
|
||||
|
||||
bucketName := opts.Bucket
|
||||
objectName := strings.Trim(req.URL.Path, "/")
|
||||
canonicalizedResource := fmt.Sprintf("/%s/%s", bucketName, objectName)
|
||||
if bucketName == "" && objectName == "" {
|
||||
canonicalizedResource = "/"
|
||||
}
|
||||
if len(req.URL.Query()) > 0 {
|
||||
query := req.URL.Query()
|
||||
|
||||
keys := make([]string, 0, len(query))
|
||||
for key := range query {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for i, key := range keys {
|
||||
if i == 0 {
|
||||
canonicalizedResource += "?"
|
||||
} else {
|
||||
canonicalizedResource += "&"
|
||||
}
|
||||
|
||||
value := query.Get(key)
|
||||
if value == "" {
|
||||
canonicalizedResource += key
|
||||
} else {
|
||||
canonicalizedResource += fmt.Sprintf("%s=%s", strings.ToLower(key), url.QueryEscape(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
method := strings.ToUpper(req.Method)
|
||||
|
||||
dateStr := time.Now().UTC().Format(http.TimeFormat)
|
||||
|
||||
contentMd5 := req.Header.Get("Content-MD5")
|
||||
contentType := req.Header.Get("Content-Type")
|
||||
|
||||
stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s\n%s%s", method, contentMd5, contentType, dateStr, canonicalizedHeaders, canonicalizedResource)
|
||||
println("stringToSign:", stringToSign)
|
||||
|
||||
h := hmac.New(sha1.New, []byte(opts.SecretAccessKey))
|
||||
h.Write([]byte(stringToSign))
|
||||
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("OBS %s:%s", opts.AccessKeyId, signature))
|
||||
req.Header.Set("Date", dateStr)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return &Client{rc: restyClient}, nil
|
||||
}
|
||||
|
||||
func (c *Client) SetTimeout(timeout time.Duration) *Client {
|
||||
c.rc.SetTimeout(timeout)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Client) newRequest(method string, path string, params any) (*resty.Request, error) {
|
||||
if method == "" {
|
||||
return nil, fmt.Errorf("sdkerr: unset method")
|
||||
}
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("sdkerr: unset path")
|
||||
}
|
||||
|
||||
requestUrl, err := url.Parse(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sdkerr: invalid path: %w", err)
|
||||
} else if requestUrl.IsAbs() {
|
||||
return nil, fmt.Errorf("sdkerr: path should be relative")
|
||||
}
|
||||
|
||||
payloadStr := ""
|
||||
contentType := ""
|
||||
if params != nil {
|
||||
payloadb, err := xml.Marshal(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payloadStr = string(payloadb)
|
||||
contentType = "application/xml"
|
||||
}
|
||||
payloadMd5 := md5.Sum([]byte(payloadStr))
|
||||
payloadMd5Encoded := base64.StdEncoding.EncodeToString(payloadMd5[:])
|
||||
|
||||
req := c.rc.R()
|
||||
req.Method = method
|
||||
req.URL = requestUrl.Path
|
||||
req.QueryParam = requestUrl.Query()
|
||||
req.SetHeader("Content-MD5", payloadMd5Encoded)
|
||||
req.SetHeader("Content-Type", contentType)
|
||||
req.SetBody(payloadStr)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {
|
||||
if req == nil {
|
||||
return nil, fmt.Errorf("sdkerr: nil request")
|
||||
}
|
||||
|
||||
// WARN:
|
||||
// PLEASE DO NOT USE `req.SetBody` HERE! USE `newRequest` INSTEAD.
|
||||
// PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.
|
||||
|
||||
resp, err := req.Send()
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("sdkerr: failed to send request: %w", err)
|
||||
} else if resp.IsError() {
|
||||
return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String())
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {
|
||||
if req == nil {
|
||||
return nil, fmt.Errorf("sdkerr: nil request")
|
||||
}
|
||||
|
||||
resp, err := c.doRequest(req)
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
xml.Unmarshal(resp.Body(), &res)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
if len(resp.Body()) != 0 {
|
||||
if err := xml.Unmarshal(resp.Body(), &res); err != nil {
|
||||
return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String())
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package obs
|
||||
|
||||
type Options struct {
|
||||
AccessKeyId string
|
||||
SecretAccessKey string
|
||||
Region string
|
||||
Bucket string
|
||||
}
|
||||
|
||||
type OptionsFunc func(*Options)
|
||||
|
||||
func WithAkSk(ak, sk string) OptionsFunc {
|
||||
return func(o *Options) {
|
||||
o.AccessKeyId = ak
|
||||
o.SecretAccessKey = sk
|
||||
}
|
||||
}
|
||||
|
||||
func WithRegion(region string) OptionsFunc {
|
||||
return func(o *Options) {
|
||||
o.Region = region
|
||||
}
|
||||
}
|
||||
|
||||
func WithBucket(bucket string) OptionsFunc {
|
||||
return func(o *Options) {
|
||||
o.Bucket = bucket
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package obs
|
||||
|
||||
type sdkResponse interface{}
|
||||
|
||||
type sdkResponseBase struct{}
|
||||
|
||||
var _ sdkResponse = (*sdkResponseBase)(nil)
|
||||
Reference in New Issue
Block a user