Files
certimate/pkg/core/deployer/providers/nginxproxymanager/nginxproxymanager.go
T

409 lines
13 KiB
Go

package nginxproxymanager
import (
"context"
"crypto/tls"
"fmt"
"log/slog"
"strconv"
"time"
"github.com/samber/lo"
"github.com/certimate-go/certimate/pkg/core"
cmgrimpl "github.com/certimate-go/certimate/pkg/core/certmgr/providers/nginxproxymanager"
npmsdk "github.com/certimate-go/certimate/pkg/sdk3rd/nginxproxymanager"
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
xloop "github.com/certimate-go/certimate/pkg/utils/loop"
xwait "github.com/certimate-go/certimate/pkg/utils/wait"
)
type (
Provider = core.Deployer
DeployResult = core.DeployerDeployResult
)
type DeployerConfig struct {
// NPM 服务地址。
ServerUrl string `json:"serverUrl"`
// NPM API 认证方式。
// 可取值 "password"、"token"。
// 零值时默认值 [AUTH_METHOD_PASSWORD]。
AuthMethod string `json:"authMethod,omitempty"`
// NPM 用户名。
Username string `json:"username,omitempty"`
// NPM 密码。
Password string `json:"password,omitempty"`
// NPM API Token。
ApiToken string `json:"apiToken,omitempty"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
// 部署目标。
DeployTarget string `json:"deployTarget"`
// 域名匹配模式。
// 零值时默认值 [HOST_MATCH_PATTERN_SPECIFIED]。
HostMatchPattern string `json:"hostMatchPattern,omitempty"`
// 主机类型。
// 部署目标为 [DEPLOY_TARGET_HOST] 时必填。
HostType string `json:"hostType,omitempty"`
// 主机 ID。
// 部署目标为 [DEPLOY_TARGET_HOST]、且匹配模式非 [HOST_MATCH_PATTERN_CERTSAN] 时必填。
HostId int64 `json:"hostId,omitempty"`
// 证书 ID。
// 部署目标为 [DEPLOY_TARGET_CERTIFICATE] 时必填。
CertificateId int64 `json:"certificateId,omitempty"`
}
type Deployer struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *npmsdk.Client
sdkCertmgr core.Certmgr
}
var _ Provider = (*Deployer)(nil)
func NewDeployer(config *DeployerConfig) (*Deployer, error) {
if config == nil {
return nil, fmt.Errorf("the configuration of the deployer provider is nil")
}
client, err := createSDKClient(config.ServerUrl, config.AuthMethod, config.Username, config.Password, config.ApiToken, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("could not create client: %w", err)
}
pcertmgr, err := cmgrimpl.NewCertmgr(&cmgrimpl.CertmgrConfig{
ServerUrl: config.ServerUrl,
AuthMethod: config.AuthMethod,
Username: config.Username,
Password: config.Password,
ApiToken: config.ApiToken,
AllowInsecureConnections: config.AllowInsecureConnections,
})
if err != nil {
return nil, fmt.Errorf("could not create certmgr: %w", err)
}
return &Deployer{
config: config,
logger: slog.Default(),
sdkClient: client,
sdkCertmgr: pcertmgr,
}, nil
}
func (d *Deployer) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sdkCertmgr.SetLogger(logger)
}
func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*DeployResult, error) {
// 根据部署目标决定业务流程
switch d.config.DeployTarget {
case DEPLOY_TARGET_HOST:
if err := d.deployToHost(ctx, certPEM, privkeyPEM); err != nil {
return nil, err
}
case DEPLOY_TARGET_CERTIFICATE:
if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported deploy target '%s'", d.config.DeployTarget)
}
return &DeployResult{}, nil
}
func (d *Deployer) deployToHost(ctx context.Context, certPEM, privkeyPEM string) error {
// 解析证书内容
certX509, err := xcert.ParseCertificateFromPEM(certPEM)
if err != nil {
return err
}
// 上传证书
upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)
if err != nil {
return fmt.Errorf("failed to upload certificate file: %w", err)
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
// 获取全部可部署的主机列表
hostsByType, err := d.getAllHosts(ctx, d.config.HostType)
if err != nil {
return err
}
// 获取待部署的主机列表
var hostIds []int64
switch d.config.HostMatchPattern {
case "", HOST_MATCH_PATTERN_SPECIFIED:
{
if d.config.HostId == 0 {
return fmt.Errorf("config `hostId` is required")
}
hostIds = []int64{d.config.HostId}
}
case HOST_MATCH_PATTERN_CERTSAN:
{
hostCandidates := hostsByType
hostIds = lo.Map(
lo.Filter(hostCandidates, func(hostItem *npmsdk.Host, _ int) bool {
return len(hostItem.DomainNames) > 0 &&
lo.EveryBy(hostItem.DomainNames, func(domain string) bool {
return certX509.VerifyHostname(domain) == nil
})
}),
func(hostItem *npmsdk.Host, _ int) int64 {
return hostItem.Id
},
)
if len(hostIds) == 0 {
return fmt.Errorf("could not find any hosts matched by certificate")
}
}
default:
return fmt.Errorf("unsupported host match pattern: '%s'", d.config.HostMatchPattern)
}
// 批量更新主机证书
if len(hostIds) == 0 {
d.logger.Info("no hosts to deploy")
} else {
d.logger.Info("found hosts to deploy", slog.Any("hostIds", hostIds))
// 跳过已部署过的主机
hostIds = lo.Filter(hostIds, func(hostId int64, _ int) bool {
hostInfo, _ := lo.Find(hostsByType, func(hostItem *npmsdk.Host) bool {
return hostId == hostItem.Id
})
if hostInfo != nil {
certId, _ := strconv.ParseInt(upres.CertId, 10, 64)
return hostInfo.CertificateId != certId
}
return true
})
if err := xloop.ForRangeAllWithContext(ctx, hostIds, func(ctx context.Context, hostId int64, i int) error {
if i > 0 {
if err := xwait.DelayWithContext(ctx, 3*time.Second); err != nil {
return err
}
}
certId, _ := strconv.ParseInt(upres.CertId, 10, 64)
return d.updateHostCertificate(ctx, d.config.HostType, hostId, certId)
}); err != nil {
return err
}
}
return nil
}
func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error {
if d.config.CertificateId == 0 {
return fmt.Errorf("config `certificateId` is required")
}
// 替换证书
rplres, err := d.sdkCertmgr.Replace(ctx, strconv.FormatInt(d.config.CertificateId, 10), certPEM, privkeyPEM)
if err != nil {
return fmt.Errorf("failed to replace certificate file: %w", err)
} else {
d.logger.Info("ssl certificate replaced", slog.Any("result", rplres))
}
// 获取默认站点
settingsGetDefaultSiteReq := &npmsdk.SettingsGetDefaultSiteRequest{}
settingsGetDefaultSiteResp, err := d.sdkClient.SettingsGetDefaultSiteWithContext(ctx, settingsGetDefaultSiteReq)
d.logger.Debug("sdk request 'settings.GetDefaultSite'", slog.Any("request", settingsGetDefaultSiteReq), slog.Any("response", settingsGetDefaultSiteResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'settings.GetDefaultSite': %w", err)
}
// 更新默认站点,以触发 nginx 重启
settingsSetDefaultSiteReq := &npmsdk.SettingsSetDefaultSiteRequest{
Value: settingsGetDefaultSiteResp.Value,
}
settingsSetDefaultSiteResp, err := d.sdkClient.SettingsSetDefaultSiteWithContext(ctx, settingsSetDefaultSiteReq)
d.logger.Debug("sdk request 'settings.SetDefaultSite'", slog.Any("request", settingsSetDefaultSiteReq), slog.Any("response", settingsSetDefaultSiteResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'settings.SetDefaultSite': %w", err)
}
return nil
}
func (d *Deployer) getAllHosts(ctx context.Context, cloudHostType string) ([]*npmsdk.Host, error) {
var hosts []*npmsdk.Host
switch cloudHostType {
case HOST_TYPE_PROXY:
{
nginxListProxyHostsReq := &npmsdk.NginxListProxyHostsRequest{}
nginxListProxyHostsResp, err := d.sdkClient.NginxListProxyHostsWithContext(ctx, nginxListProxyHostsReq)
d.logger.Debug("sdk request 'nginx.ListProxyHosts'", slog.Any("request", nginxListProxyHostsReq), slog.Any("response", nginxListProxyHostsResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'nginx.ListProxyHosts': %w", err)
}
hosts = make([]*npmsdk.Host, 0, len(*nginxListProxyHostsResp))
for _, hostItem := range *nginxListProxyHostsResp {
hosts = append(hosts, &hostItem.Host)
}
}
case HOST_TYPE_REDIRECTION:
{
nginxListRedirectionHostsReq := &npmsdk.NginxListRedirectionHostsRequest{}
nginxListRedirectionHostsResp, err := d.sdkClient.NginxListRedirectionHostsWithContext(ctx, nginxListRedirectionHostsReq)
d.logger.Debug("sdk request 'nginx.ListRedirectionHosts'", slog.Any("request", nginxListRedirectionHostsReq), slog.Any("response", nginxListRedirectionHostsResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'nginx.ListRedirectionHosts': %w", err)
}
hosts = make([]*npmsdk.Host, 0, len(*nginxListRedirectionHostsResp))
for _, hostItem := range *nginxListRedirectionHostsResp {
hosts = append(hosts, &hostItem.Host)
}
}
case HOST_TYPE_STREAM:
{
nginxListStreamsReq := &npmsdk.NginxListStreamsRequest{}
nginxListStreamsResp, err := d.sdkClient.NginxListStreamsWithContext(ctx, nginxListStreamsReq)
d.logger.Debug("sdk request 'nginx.ListStreams'", slog.Any("request", nginxListStreamsReq), slog.Any("response", nginxListStreamsResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'nginx.ListStreams': %w", err)
}
hosts = make([]*npmsdk.Host, 0, len(*nginxListStreamsResp))
for _, hostItem := range *nginxListStreamsResp {
hosts = append(hosts, &hostItem.Host)
}
}
case HOST_TYPE_DEAD:
{
nginxListDeadHostsReq := &npmsdk.NginxListDeadHostsRequest{}
nginxListDeadHostsResp, err := d.sdkClient.NginxListDeadHostsWithContext(ctx, nginxListDeadHostsReq)
d.logger.Debug("sdk request 'nginx.ListDeadHosts'", slog.Any("request", nginxListDeadHostsReq), slog.Any("response", nginxListDeadHostsResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'nginx.ListDeadHosts': %w", err)
}
hosts = make([]*npmsdk.Host, 0, len(*nginxListDeadHostsResp))
for _, hostItem := range *nginxListDeadHostsResp {
hosts = append(hosts, &hostItem.Host)
}
}
default:
return hosts, fmt.Errorf("unsupported host type: '%s'", cloudHostType)
}
return hosts, nil
}
func (d *Deployer) updateHostCertificate(ctx context.Context, cloudHostType string, cloudHostId int64, cloudCertId int64) error {
switch cloudHostType {
case HOST_TYPE_PROXY:
{
nginxUpdateProxyHostReq := &npmsdk.NginxUpdateProxyHostRequest{
CertificateId: lo.ToPtr(cloudCertId),
}
nginxUpdateProxyHostResp, err := d.sdkClient.NginxUpdateProxyHostWithContext(ctx, cloudHostId, nginxUpdateProxyHostReq)
d.logger.Debug("sdk request 'nginx.UpdateProxyHost'", slog.Int64("params.hostId", cloudHostId), slog.Any("request", nginxUpdateProxyHostReq), slog.Any("response", nginxUpdateProxyHostResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'nginx.UpdateProxyHost': %w", err)
}
}
case HOST_TYPE_REDIRECTION:
{
nginxUpdateRedirectionHostReq := &npmsdk.NginxUpdateRedirectionHostRequest{
CertificateId: lo.ToPtr(cloudCertId),
}
nginxUpdateRedirectionHostResp, err := d.sdkClient.NginxUpdateRedirectionHostWithContext(ctx, cloudHostId, nginxUpdateRedirectionHostReq)
d.logger.Debug("sdk request 'nginx.UpdateRedirectionHost'", slog.Int64("params.hostId", cloudHostId), slog.Any("request", nginxUpdateRedirectionHostReq), slog.Any("response", nginxUpdateRedirectionHostResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'nginx.UpdateRedirectionHost': %w", err)
}
}
case HOST_TYPE_STREAM:
{
nginxUpdateStreamReq := &npmsdk.NginxUpdateStreamRequest{
CertificateId: lo.ToPtr(cloudCertId),
}
nginxUpdateStreamResp, err := d.sdkClient.NginxUpdateStreamWithContext(ctx, cloudHostId, nginxUpdateStreamReq)
d.logger.Debug("sdk request 'nginx.UpdateStream'", slog.Int64("params.hostId", cloudHostId), slog.Any("request", nginxUpdateStreamReq), slog.Any("response", nginxUpdateStreamResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'nginx.UpdateStream': %w", err)
}
}
case HOST_TYPE_DEAD:
{
nginxUpdateDeadHostReq := &npmsdk.NginxUpdateDeadHostRequest{
CertificateId: lo.ToPtr(cloudCertId),
}
nginxUpdateDeadHostResp, err := d.sdkClient.NginxUpdateDeadHostWithContext(ctx, cloudHostId, nginxUpdateDeadHostReq)
d.logger.Debug("sdk request 'nginx.UpdateDeadHost'", slog.Int64("params.hostId", cloudHostId), slog.Any("request", nginxUpdateDeadHostReq), slog.Any("response", nginxUpdateDeadHostResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'nginx.UpdateDeadHost': %w", err)
}
}
default:
return fmt.Errorf("unsupported host type: '%s'", cloudHostType)
}
return nil
}
func createSDKClient(serverUrl, authMethod, username, password, apiToken string, skipTlsVerify bool) (*npmsdk.Client, error) {
var client *npmsdk.Client
var err error
switch authMethod {
case "", AUTH_METHOD_PASSWORD:
{
client, err = npmsdk.NewClient(serverUrl,
npmsdk.WithLogins(username, password),
)
}
case AUTH_METHOD_TOKEN:
{
client, err = npmsdk.NewClient(serverUrl,
npmsdk.WithJwtToken(apiToken),
)
}
}
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}