feat: implement local, ssh, webhook Deployer

This commit is contained in:
Fu Diwei
2024-11-19 19:09:48 +08:00
parent aa7fb7da06
commit 6367785b1b
7 changed files with 647 additions and 5 deletions

View File

@@ -0,0 +1,17 @@
package local
type OutputFormatType string
const (
OUTPUT_FORMAT_PEM = OutputFormatType("PEM")
OUTPUT_FORMAT_PFX = OutputFormatType("PFX")
OUTPUT_FORMAT_JKS = OutputFormatType("JKS")
)
type ShellEnvType string
const (
SHELL_ENV_SH = ShellEnvType("sh")
SHELL_ENV_CMD = ShellEnvType("cmd")
SHELL_ENV_POWERSHELL = ShellEnvType("powershell")
)

View File

@@ -0,0 +1,177 @@
package local
import (
"bytes"
"context"
"errors"
"fmt"
"os/exec"
"runtime"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/utils/fs"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
type LocalDeployerConfig struct {
// Shell 执行环境。
// 空值时默认根据操作系统决定。
ShellEnv ShellEnvType `json:"shellEnv,omitempty"`
// 前置命令。
PreCommand string `json:"preCommand,omitempty"`
// 后置命令。
PostCommand string `json:"postCommand,omitempty"`
// 输出证书格式。
// 空值时默认为 [OUTPUT_FORMAT_PEM]。
OutputFormat OutputFormatType `json:"outputFormat,omitempty"`
// 输出证书文件路径。
OutputCertPath string `json:"outputCertPath,omitempty"`
// 输出私钥文件路径。
OutputKeyPath string `json:"outputKeyPath,omitempty"`
// PFX 导出密码。
// 证书格式为 PFX 时必填。
PfxPassword string `json:"pfxPassword,omitempty"`
// JKS 别名。
// 证书格式为 JKS 时必填。
JksAlias string `json:"jksAlias,omitempty"`
// JKS 密钥密码。
// 证书格式为 JKS 时必填。
JksKeypass string `json:"jksKeypass,omitempty"`
// JKS 存储密码。
// 证书格式为 JKS 时必填。
JksStorepass string `json:"jksStorepass,omitempty"`
}
type LocalDeployer struct {
config *LocalDeployerConfig
logger deployer.Logger
}
func New(config *LocalDeployerConfig) (*LocalDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *LocalDeployerConfig, logger deployer.Logger) (*LocalDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
return &LocalDeployer{
logger: logger,
config: config,
}, nil
}
func (d *LocalDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 执行前置命令
if d.config.PreCommand != "" {
stdout, stderr, err := execCommand(d.config.ShellEnv, d.config.PreCommand)
if err != nil {
return nil, xerrors.Wrapf(err, "failed to run pre-command, stdout: %s, stderr: %s", stdout, stderr)
}
d.logger.Appendt("pre-command executed", stdout)
}
// 写入证书和私钥文件
switch d.config.OutputFormat {
case "", OUTPUT_FORMAT_PEM:
if err := fs.WriteFileString(d.config.OutputCertPath, certPem); err != nil {
return nil, err
}
d.logger.Appendt("certificate file saved")
if err := fs.WriteFileString(d.config.OutputKeyPath, privkeyPem); err != nil {
return nil, err
}
d.logger.Appendt("private key file saved")
case OUTPUT_FORMAT_PFX:
pfxData, err := x509.TransformCertificateFromPEMToPFX(certPem, privkeyPem, d.config.PfxPassword)
if err != nil {
return nil, err
}
d.logger.Appendt("certificate transformed to PFX")
if err := fs.WriteFile(d.config.OutputCertPath, pfxData); err != nil {
return nil, err
}
d.logger.Appendt("certificate file saved")
case OUTPUT_FORMAT_JKS:
jksData, err := x509.TransformCertificateFromPEMToJKS(certPem, privkeyPem, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass)
if err != nil {
return nil, err
}
d.logger.Appendt("certificate transformed to JKS")
if err := fs.WriteFile(d.config.OutputCertPath, jksData); err != nil {
return nil, err
}
d.logger.Appendt("certificate file uploaded")
default:
return nil, fmt.Errorf("unsupported output format: %s", d.config.OutputFormat)
}
// 执行后置命令
if d.config.PostCommand != "" {
stdout, stderr, err := execCommand(d.config.ShellEnv, d.config.PostCommand)
if err != nil {
return nil, xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr)
}
d.logger.Appendt("post-command executed", stdout)
}
return &deployer.DeployResult{}, nil
}
func execCommand(shellEnv ShellEnvType, command string) (string, string, error) {
var cmd *exec.Cmd
switch shellEnv {
case SHELL_ENV_SH:
cmd = exec.Command("sh", "-c", command)
case SHELL_ENV_CMD:
cmd = exec.Command("cmd", "/C", command)
case SHELL_ENV_POWERSHELL:
cmd = exec.Command("powershell", "-Command", command)
case "":
if runtime.GOOS == "windows" {
cmd = exec.Command("cmd", "/C", command)
} else {
cmd = exec.Command("sh", "-c", command)
}
default:
return "", "", fmt.Errorf("unsupported shell env: %s", shellEnv)
}
var stdoutBuf bytes.Buffer
cmd.Stdout = &stdoutBuf
var stderrBuf bytes.Buffer
cmd.Stderr = &stderrBuf
err := cmd.Run()
if err != nil {
return "", "", xerrors.Wrap(err, "failed to execute shell command")
}
return stdoutBuf.String(), stderrBuf.String(), nil
}

View File

@@ -0,0 +1,9 @@
package ssh
type OutputFormatType string
const (
OUTPUT_FORMAT_PEM = OutputFormatType("PEM")
OUTPUT_FORMAT_PFX = OutputFormatType("PFX")
OUTPUT_FORMAT_JKS = OutputFormatType("JKS")
)

View File

@@ -0,0 +1,251 @@
package ssh
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"path/filepath"
xerrors "github.com/pkg/errors"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
type SshDeployerConfig struct {
// SSH 主机。
// 空值时默认为 "localhost"。
SshHost string `json:"sshHost,omitempty"`
// SSH 端口。
// 空值时默认为 22。
SshPort int32 `json:"sshPort,omitempty"`
// SSH 登录用户名。
SshUsername string `json:"sshUsername,omitempty"`
// SSH 登录密码。
SshPassword string `json:"sshPassword,omitempty"`
// SSH 登录私钥。
SshKey string `json:"sshKey,omitempty"`
// SSH 登录私钥口令。
SshKeyPassphrase string `json:"sshKeyPassphrase,omitempty"`
// 前置命令。
PreCommand string `json:"preCommand,omitempty"`
// 后置命令。
PostCommand string `json:"postCommand,omitempty"`
// 输出证书格式。
// 空值时默认为 [OUTPUT_FORMAT_PEM]。
OutputFormat OutputFormatType `json:"outputFormat,omitempty"`
// 输出证书文件路径。
OutputCertPath string `json:"outputCertPath,omitempty"`
// 输出私钥文件路径。
OutputKeyPath string `json:"outputKeyPath,omitempty"`
// PFX 导出密码。
// 证书格式为 PFX 时必填。
PfxPassword string `json:"pfxPassword,omitempty"`
// JKS 别名。
// 证书格式为 JKS 时必填。
JksAlias string `json:"jksAlias,omitempty"`
// JKS 密钥密码。
// 证书格式为 JKS 时必填。
JksKeypass string `json:"jksKeypass,omitempty"`
// JKS 存储密码。
// 证书格式为 JKS 时必填。
JksStorepass string `json:"jksStorepass,omitempty"`
}
type SshDeployer struct {
config *SshDeployerConfig
logger deployer.Logger
}
func New(config *SshDeployerConfig) (*SshDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *SshDeployerConfig, logger deployer.Logger) (*SshDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
return &SshDeployer{
logger: logger,
config: config,
}, nil
}
func (d *SshDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 连接
client, err := createSshClient(
d.config.SshHost,
d.config.SshPort,
d.config.SshUsername,
d.config.SshPassword,
d.config.SshKey,
d.config.SshKeyPassphrase,
)
if err != nil {
return nil, err
}
defer client.Close()
d.logger.Appendt("SSH connected")
// 执行前置命令
if d.config.PreCommand != "" {
stdout, stderr, err := execSshCommand(client, d.config.PreCommand)
if err != nil {
return nil, xerrors.Wrapf(err, "failed to run pre-command: stdout: %s, stderr: %s", stdout, stderr)
}
d.logger.Appendt("SSH pre-command executed", stdout)
}
// 上传证书和私钥文件
switch d.config.OutputFormat {
case "", OUTPUT_FORMAT_PEM:
if err := writeSftpFileString(client, d.config.OutputCertPath, certPem); err != nil {
return nil, err
}
d.logger.Appendt("certificate file uploaded")
if err := writeSftpFileString(client, d.config.OutputKeyPath, privkeyPem); err != nil {
return nil, err
}
d.logger.Appendt("private key file uploaded")
case OUTPUT_FORMAT_PFX:
pfxData, err := x509.TransformCertificateFromPEMToPFX(certPem, privkeyPem, d.config.PfxPassword)
if err != nil {
return nil, err
}
d.logger.Appendt("certificate transformed to PFX")
if err := writeSftpFile(client, d.config.OutputCertPath, pfxData); err != nil {
return nil, err
}
d.logger.Appendt("certificate file uploaded")
case OUTPUT_FORMAT_JKS:
jksData, err := x509.TransformCertificateFromPEMToJKS(certPem, privkeyPem, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass)
if err != nil {
return nil, err
}
d.logger.Appendt("certificate transformed to JKS")
if err := writeSftpFile(client, d.config.OutputCertPath, jksData); err != nil {
return nil, err
}
d.logger.Appendt("certificate file uploaded")
default:
return nil, fmt.Errorf("unsupported output format: %s", d.config.OutputFormat)
}
// 执行后置命令
if d.config.PostCommand != "" {
stdout, stderr, err := execSshCommand(client, d.config.PostCommand)
if err != nil {
return nil, xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr)
}
d.logger.Appendt("SSH post-command executed", stdout)
}
return &deployer.DeployResult{}, nil
}
func createSshClient(host string, port int32, username string, password string, key string, keyPassphrase string) (*ssh.Client, error) {
if host == "" {
host = "localhost"
}
if port == 0 {
port = 22
}
var authMethod ssh.AuthMethod
if key != "" {
var signer ssh.Signer
var err error
if keyPassphrase != "" {
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(key), []byte(keyPassphrase))
} else {
signer, err = ssh.ParsePrivateKey([]byte(key))
}
if err != nil {
return nil, err
}
authMethod = ssh.PublicKeys(signer)
} else {
authMethod = ssh.Password(password)
}
return ssh.Dial("tcp", fmt.Sprintf("%s:%d", host, port), &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{authMethod},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
})
}
func execSshCommand(sshCli *ssh.Client, command string) (string, string, error) {
session, err := sshCli.NewSession()
if err != nil {
return "", "", xerrors.Wrap(err, "failed to create ssh session")
}
defer session.Close()
var stdoutBuf bytes.Buffer
session.Stdout = &stdoutBuf
var stderrBuf bytes.Buffer
session.Stderr = &stderrBuf
err = session.Run(command)
if err != nil {
return "", "", xerrors.Wrap(err, "failed to execute ssh command")
}
return stdoutBuf.String(), stderrBuf.String(), nil
}
func writeSftpFileString(sshCli *ssh.Client, path string, content string) error {
return writeSftpFile(sshCli, path, []byte(content))
}
func writeSftpFile(sshCli *ssh.Client, path string, data []byte) error {
sftpCli, err := sftp.NewClient(sshCli)
if err != nil {
return xerrors.Wrap(err, "failed to create sftp client")
}
defer sftpCli.Close()
if err := sftpCli.MkdirAll(filepath.Dir(path)); err != nil {
return xerrors.Wrap(err, "failed to create remote directory")
}
file, err := sftpCli.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
if err != nil {
return xerrors.Wrap(err, "failed to open remote file")
}
defer file.Close()
_, err = file.Write(data)
if err != nil {
return xerrors.Wrap(err, "failed to write to remote file")
}
return nil
}

View File

@@ -0,0 +1,81 @@
package webhook
import (
"bytes"
"context"
"encoding/json"
"errors"
"net/http"
"strings"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
xhttp "github.com/usual2970/certimate/internal/utils/http"
)
type WebhookDeployerConfig struct {
// Webhook URL。
Url string `json:"url"`
// Webhook 变量字典。
Variables map[string]string `json:"variables,omitempty"`
}
type WebhookDeployer struct {
config *WebhookDeployerConfig
logger deployer.Logger
}
var _ deployer.Deployer = (*WebhookDeployer)(nil)
func New(config *WebhookDeployerConfig) (*WebhookDeployer, error) {
return NewWithLogger(config, deployer.NewNilLogger())
}
func NewWithLogger(config *WebhookDeployerConfig, logger deployer.Logger) (*WebhookDeployer, error) {
if config == nil {
return nil, errors.New("config is nil")
}
if logger == nil {
return nil, errors.New("logger is nil")
}
return &WebhookDeployer{
config: config,
logger: logger,
}, nil
}
type webhookData struct {
SubjectAltNames string `json:"subjectAltNames"`
Certificate string `json:"certificate"`
PrivateKey string `json:"privateKey"`
Variables map[string]string `json:"variables"`
}
func (d *WebhookDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
certX509, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
data := &webhookData{
SubjectAltNames: strings.Join(certX509.DNSNames, ","),
Certificate: certPem,
PrivateKey: privkeyPem,
Variables: d.config.Variables,
}
body, _ := json.Marshal(data)
resp, err := xhttp.Req(d.config.Url, http.MethodPost, bytes.NewReader(body), map[string]string{
"Content-Type": "application/json",
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to send webhook request")
}
d.logger.Appendt("Webhook Response", string(resp))
return &deployer.DeployResult{}, nil
}