diff --git a/internal/applicant/acme_user.go b/internal/applicant/acme_user.go index 1ab4c424..f8e80a03 100644 --- a/internal/applicant/acme_user.go +++ b/internal/applicant/acme_user.go @@ -1,6 +1,7 @@ package applicant import ( + "context" "crypto" "crypto/ecdsa" "crypto/elliptic" @@ -110,14 +111,11 @@ func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderCon Kid: sslProviderConfig.Config.GoogleTrustServices.EabKid, HmacEncoded: sslProviderConfig.Config.GoogleTrustServices.EabHmacKey, }) - case sslProviderLetsEncrypt, sslProviderLetsEncryptStaging: reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) - default: err = fmt.Errorf("unsupported ssl provider: %s", sslProviderConfig.Provider) } - if err != nil { return nil, err } @@ -129,7 +127,12 @@ func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderCon return resp.Resource, nil } - if err := repo.Save(sslProviderConfig.Provider, user.GetEmail(), user.getPrivateKeyPEM(), reg); err != nil { + if _, err := repo.Save(context.Background(), &domain.AcmeAccount{ + CA: sslProviderConfig.Provider, + Email: user.GetEmail(), + Key: user.getPrivateKeyPEM(), + Resource: reg, + }); err != nil { return nil, fmt.Errorf("failed to save registration: %w", err) } diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go index a612ebda..c565e1ff 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -26,6 +26,7 @@ type ApplyCertResult struct { CertificateFullChain string IssuerCertificate string PrivateKey string + ACMEAccountUrl string ACMECertUrl string ACMECertStableUrl string CSR string @@ -46,8 +47,7 @@ type applicantOptions struct { DnsPropagationTimeout int32 DnsTTL int32 DisableFollowCNAME bool - DisableARI bool - SkipBeforeExpiryDays int32 + ReplacedARIAccount string ReplacedARICertId string } @@ -67,8 +67,6 @@ func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) { DnsPropagationTimeout: nodeConfig.DnsPropagationTimeout, DnsTTL: nodeConfig.DnsTTL, DisableFollowCNAME: nodeConfig.DisableFollowCNAME, - DisableARI: nodeConfig.DisableARI, - SkipBeforeExpiryDays: nodeConfig.SkipBeforeExpiryDays, } accessRepo := repository.NewAccessRepository() @@ -95,6 +93,7 @@ func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) { lastCertX509, _ := certcrypto.ParsePEMCertificate([]byte(lastCertificate.Certificate)) if lastCertX509 != nil { replacedARICertId, _ := certificate.MakeARICertID(lastCertX509) + options.ReplacedARIAccount = lastCertificate.ACMEAccountUrl options.ReplacedARICertId = replacedARICertId } } @@ -141,7 +140,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap // Create an ACME client config config := lego.NewConfig(acmeUser) config.CADirURL = sslProviderUrls[sslProviderConfig.Provider] - config.Certificate.KeyType = parseKeyAlgorithm(options.KeyAlgorithm) + config.Certificate.KeyType = parseKeyAlgorithm(domain.CertificateKeyAlgorithmType(options.KeyAlgorithm)) // Create an ACME client client, err := lego.NewClient(config) @@ -171,7 +170,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap Domains: options.Domains, Bundle: true, } - if !options.DisableARI { + if options.ReplacedARICertId != "" && options.ReplacedARIAccount != acmeUser.Registration.URI { certRequest.ReplacesCertID = options.ReplacedARICertId } certResource, err := client.Certificate.Obtain(certRequest) @@ -183,29 +182,30 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap CertificateFullChain: strings.TrimSpace(string(certResource.Certificate)), IssuerCertificate: strings.TrimSpace(string(certResource.IssuerCertificate)), PrivateKey: strings.TrimSpace(string(certResource.PrivateKey)), + ACMEAccountUrl: acmeUser.Registration.URI, ACMECertUrl: certResource.CertURL, ACMECertStableUrl: certResource.CertStableURL, CSR: strings.TrimSpace(string(certResource.CSR)), }, nil } -func parseKeyAlgorithm(algo string) certcrypto.KeyType { +func parseKeyAlgorithm(algo domain.CertificateKeyAlgorithmType) certcrypto.KeyType { switch algo { - case "RSA2048": + case domain.CertificateKeyAlgorithmTypeRSA2048: return certcrypto.RSA2048 - case "RSA3072": + case domain.CertificateKeyAlgorithmTypeRSA3072: return certcrypto.RSA3072 - case "RSA4096": + case domain.CertificateKeyAlgorithmTypeRSA4096: return certcrypto.RSA4096 - case "RSA8192": + case domain.CertificateKeyAlgorithmTypeRSA8192: return certcrypto.RSA8192 - case "EC256": + case domain.CertificateKeyAlgorithmTypeEC256: return certcrypto.EC256 - case "EC384": + case domain.CertificateKeyAlgorithmTypeEC384: return certcrypto.EC384 - default: - return certcrypto.RSA2048 } + + return certcrypto.RSA2048 } // TODO: 暂时使用代理模式以兼容之前版本代码,后续重新实现此处逻辑 diff --git a/internal/domain/certificate.go b/internal/domain/certificate.go index f0e03711..1abecb9c 100644 --- a/internal/domain/certificate.go +++ b/internal/domain/certificate.go @@ -1,24 +1,76 @@ package domain -import "time" +import ( + "crypto/x509" + "strings" + "time" + + "github.com/usual2970/certimate/internal/pkg/utils/certs" +) const CollectionNameCertificate = "certificate" type Certificate struct { Meta - Source CertificateSourceType `json:"source" db:"source"` - SubjectAltNames string `json:"subjectAltNames" db:"subjectAltNames"` - Certificate string `json:"certificate" db:"certificate"` - PrivateKey string `json:"privateKey" db:"privateKey"` - IssuerCertificate string `json:"issuerCertificate" db:"issuerCertificate"` - EffectAt time.Time `json:"effectAt" db:"effectAt"` - ExpireAt time.Time `json:"expireAt" db:"expireAt"` - ACMECertUrl string `json:"acmeCertUrl" db:"acmeCertUrl"` - ACMECertStableUrl string `json:"acmeCertStableUrl" db:"acmeCertStableUrl"` - WorkflowId string `json:"workflowId" db:"workflowId"` - WorkflowNodeId string `json:"workflowNodeId" db:"workflowNodeId"` - WorkflowOutputId string `json:"workflowOutputId" db:"workflowOutputId"` - DeletedAt *time.Time `json:"deleted" db:"deleted"` + Source CertificateSourceType `json:"source" db:"source"` + SubjectAltNames string `json:"subjectAltNames" db:"subjectAltNames"` + SerialNumber string `json:"serialNumber" db:"serialNumber"` + Certificate string `json:"certificate" db:"certificate"` + PrivateKey string `json:"privateKey" db:"privateKey"` + Issuer string `json:"issuer" db:"issuer"` + IssuerCertificate string `json:"issuerCertificate" db:"issuerCertificate"` + KeyAlgorithm CertificateKeyAlgorithmType `json:"keyAlgorithm" db:"keyAlgorithm"` + EffectAt time.Time `json:"effectAt" db:"effectAt"` + ExpireAt time.Time `json:"expireAt" db:"expireAt"` + ACMEAccountUrl string `json:"acmeAccountUrl" db:"acmeAccountUrl"` + ACMECertUrl string `json:"acmeCertUrl" db:"acmeCertUrl"` + ACMECertStableUrl string `json:"acmeCertStableUrl" db:"acmeCertStableUrl"` + WorkflowId string `json:"workflowId" db:"workflowId"` + WorkflowNodeId string `json:"workflowNodeId" db:"workflowNodeId"` + WorkflowOutputId string `json:"workflowOutputId" db:"workflowOutputId"` + DeletedAt *time.Time `json:"deleted" db:"deleted"` +} + +func (c *Certificate) PopulateFromX509(certX509 *x509.Certificate) *Certificate { + c.SubjectAltNames = strings.Join(certX509.DNSNames, ";") + c.SerialNumber = strings.ToUpper(certX509.SerialNumber.Text(16)) + c.Issuer = strings.Join(certX509.Issuer.Organization, ";") + c.EffectAt = certX509.NotBefore + c.ExpireAt = certX509.NotAfter + + switch certX509.SignatureAlgorithm { + case x509.SHA256WithRSA, x509.SHA256WithRSAPSS: + c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA2048 + case x509.SHA384WithRSA, x509.SHA384WithRSAPSS: + c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA3072 + case x509.SHA512WithRSA, x509.SHA512WithRSAPSS: + c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA4096 + case x509.ECDSAWithSHA256: + c.KeyAlgorithm = CertificateKeyAlgorithmTypeEC256 + case x509.ECDSAWithSHA384: + c.KeyAlgorithm = CertificateKeyAlgorithmTypeEC384 + case x509.ECDSAWithSHA512: + c.KeyAlgorithm = CertificateKeyAlgorithmTypeEC512 + default: + c.KeyAlgorithm = CertificateKeyAlgorithmType("") + } + + return c +} + +func (c *Certificate) PopulateFromPEM(certPEM, privkeyPEM string) *Certificate { + c.Certificate = certPEM + c.PrivateKey = privkeyPEM + + _, issuerCertPEM, _ := certs.ExtractCertificatesFromPEM(certPEM) + c.IssuerCertificate = issuerCertPEM + + certX509, _ := certs.ParseCertificateFromPEM(certPEM) + if certX509 != nil { + c.PopulateFromX509(certX509) + } + + return c } type CertificateSourceType string @@ -27,3 +79,15 @@ const ( CertificateSourceTypeWorkflow = CertificateSourceType("workflow") CertificateSourceTypeUpload = CertificateSourceType("upload") ) + +type CertificateKeyAlgorithmType string + +const ( + CertificateKeyAlgorithmTypeRSA2048 = CertificateKeyAlgorithmType("RSA2048") + CertificateKeyAlgorithmTypeRSA3072 = CertificateKeyAlgorithmType("RSA3072") + CertificateKeyAlgorithmTypeRSA4096 = CertificateKeyAlgorithmType("RSA4096") + CertificateKeyAlgorithmTypeRSA8192 = CertificateKeyAlgorithmType("RSA8192") + CertificateKeyAlgorithmTypeEC256 = CertificateKeyAlgorithmType("EC256") + CertificateKeyAlgorithmTypeEC384 = CertificateKeyAlgorithmType("EC384") + CertificateKeyAlgorithmTypeEC512 = CertificateKeyAlgorithmType("EC512") +) diff --git a/internal/pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn.go b/internal/pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn.go index 6591da1b..8fac6459 100644 --- a/internal/pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn.go +++ b/internal/pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn.go @@ -78,7 +78,7 @@ func (d *QiniuCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPe // 获取域名信息 // REF: https://developer.qiniu.com/fusion/4246/the-domain-name - getDomainInfoResp, err := d.sdkClient.GetDomainInfo(domain) + getDomainInfoResp, err := d.sdkClient.GetDomainInfo(context.TODO(), domain) if err != nil { return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetDomainInfo'") } @@ -88,14 +88,14 @@ func (d *QiniuCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPe // 判断域名是否已启用 HTTPS。如果已启用,修改域名证书;否则,启用 HTTPS // REF: https://developer.qiniu.com/fusion/4246/the-domain-name if getDomainInfoResp.Https != nil && getDomainInfoResp.Https.CertID != "" { - modifyDomainHttpsConfResp, err := d.sdkClient.ModifyDomainHttpsConf(domain, upres.CertId, getDomainInfoResp.Https.ForceHttps, getDomainInfoResp.Https.Http2Enable) + modifyDomainHttpsConfResp, err := d.sdkClient.ModifyDomainHttpsConf(context.TODO(), domain, upres.CertId, getDomainInfoResp.Https.ForceHttps, getDomainInfoResp.Https.Http2Enable) if err != nil { return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.ModifyDomainHttpsConf'") } d.logger.Logt("已修改域名证书", modifyDomainHttpsConfResp) } else { - enableDomainHttpsResp, err := d.sdkClient.EnableDomainHttps(domain, upres.CertId, true, true) + enableDomainHttpsResp, err := d.sdkClient.EnableDomainHttps(context.TODO(), domain, upres.CertId, true, true) if err != nil { return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.EnableDomainHttps'") } diff --git a/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go b/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go index a599cbe2..851cbf01 100644 --- a/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go +++ b/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go @@ -60,7 +60,7 @@ func (u *QiniuSSLCertUploader) Upload(ctx context.Context, certPem string, privk // 上传新证书 // REF: https://developer.qiniu.com/fusion/8593/interface-related-certificate - uploadSslCertResp, err := u.sdkClient.UploadSslCert(certName, certX509.Subject.CommonName, certPem, privkeyPem) + uploadSslCertResp, err := u.sdkClient.UploadSslCert(context.TODO(), certName, certX509.Subject.CommonName, certPem, privkeyPem) if err != nil { return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.UploadSslCert'") } diff --git a/internal/pkg/vendors/qiniu-sdk/client.go b/internal/pkg/vendors/qiniu-sdk/client.go index 4f342564..8f9d10e9 100644 --- a/internal/pkg/vendors/qiniu-sdk/client.go +++ b/internal/pkg/vendors/qiniu-sdk/client.go @@ -26,15 +26,15 @@ func NewClient(mac *auth.Credentials) *Client { return &Client{client: &client} } -func (c *Client) GetDomainInfo(domain string) (*GetDomainInfoResponse, error) { +func (c *Client) GetDomainInfo(ctx context.Context, domain string) (*GetDomainInfoResponse, error) { resp := new(GetDomainInfoResponse) - if err := c.client.Call(context.Background(), resp, http.MethodGet, c.urlf("domain/%s", domain), nil); err != nil { + if err := c.client.Call(ctx, resp, http.MethodGet, c.urlf("domain/%s", domain), nil); err != nil { return nil, err } return resp, nil } -func (c *Client) ModifyDomainHttpsConf(domain, certId string, forceHttps, http2Enable bool) (*ModifyDomainHttpsConfResponse, error) { +func (c *Client) ModifyDomainHttpsConf(ctx context.Context, domain string, certId string, forceHttps bool, http2Enable bool) (*ModifyDomainHttpsConfResponse, error) { req := &ModifyDomainHttpsConfRequest{ DomainInfoHttpsData: DomainInfoHttpsData{ CertID: certId, @@ -43,13 +43,13 @@ func (c *Client) ModifyDomainHttpsConf(domain, certId string, forceHttps, http2E }, } resp := new(ModifyDomainHttpsConfResponse) - if err := c.client.CallWithJson(context.Background(), resp, http.MethodPut, c.urlf("domain/%s/httpsconf", domain), nil, req); err != nil { + if err := c.client.CallWithJson(ctx, resp, http.MethodPut, c.urlf("domain/%s/httpsconf", domain), nil, req); err != nil { return nil, err } return resp, nil } -func (c *Client) EnableDomainHttps(domain, certId string, forceHttps, http2Enable bool) (*EnableDomainHttpsResponse, error) { +func (c *Client) EnableDomainHttps(ctx context.Context, domain string, certId string, forceHttps bool, http2Enable bool) (*EnableDomainHttpsResponse, error) { req := &EnableDomainHttpsRequest{ DomainInfoHttpsData: DomainInfoHttpsData{ CertID: certId, @@ -58,13 +58,13 @@ func (c *Client) EnableDomainHttps(domain, certId string, forceHttps, http2Enabl }, } resp := new(EnableDomainHttpsResponse) - if err := c.client.CallWithJson(context.Background(), resp, http.MethodPut, c.urlf("domain/%s/sslize", domain), nil, req); err != nil { + if err := c.client.CallWithJson(ctx, resp, http.MethodPut, c.urlf("domain/%s/sslize", domain), nil, req); err != nil { return nil, err } return resp, nil } -func (c *Client) UploadSslCert(name, commonName, certificate, privateKey string) (*UploadSslCertResponse, error) { +func (c *Client) UploadSslCert(ctx context.Context, name string, commonName string, certificate string, privateKey string) (*UploadSslCertResponse, error) { req := &UploadSslCertRequest{ Name: name, CommonName: commonName, @@ -72,7 +72,7 @@ func (c *Client) UploadSslCert(name, commonName, certificate, privateKey string) PrivateKey: privateKey, } resp := new(UploadSslCertResponse) - if err := c.client.CallWithJson(context.Background(), resp, http.MethodPost, c.urlf("sslcert"), nil, req); err != nil { + if err := c.client.CallWithJson(ctx, resp, http.MethodPost, c.urlf("sslcert"), nil, req); err != nil { return nil, err } return resp, nil diff --git a/internal/repository/acme_account.go b/internal/repository/acme_account.go index ef8ed62f..020f1aeb 100644 --- a/internal/repository/acme_account.go +++ b/internal/repository/acme_account.go @@ -1,6 +1,9 @@ package repository import ( + "context" + "database/sql" + "errors" "fmt" "github.com/go-acme/lego/v4/registration" @@ -48,18 +51,37 @@ func (r *AcmeAccountRepository) GetByCAAndEmail(ca, email string) (*domain.AcmeA return r.castRecordToModel(record) } -func (r *AcmeAccountRepository) Save(ca, email, key string, resource *registration.Resource) error { +func (r *AcmeAccountRepository) Save(ctx context.Context, acmeAccount *domain.AcmeAccount) (*domain.AcmeAccount, error) { collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameAcmeAccount) if err != nil { - return err + return acmeAccount, err } - record := core.NewRecord(collection) - record.Set("ca", ca) - record.Set("email", email) - record.Set("key", key) - record.Set("resource", resource) - return app.GetApp().Save(record) + var record *core.Record + if acmeAccount.Id == "" { + record = core.NewRecord(collection) + } else { + record, err = app.GetApp().FindRecordById(collection, acmeAccount.Id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return acmeAccount, domain.ErrRecordNotFound + } + return acmeAccount, err + } + } + + record.Set("ca", acmeAccount.CA) + record.Set("email", acmeAccount.Email) + record.Set("key", acmeAccount.Key) + record.Set("resource", acmeAccount.Resource) + if err := app.GetApp().Save(record); err != nil { + return acmeAccount, err + } + + acmeAccount.Id = record.Id + acmeAccount.CreatedAt = record.GetDateTime("created").Time() + acmeAccount.UpdatedAt = record.GetDateTime("updated").Time() + return acmeAccount, nil } func (r *AcmeAccountRepository) castRecordToModel(record *core.Record) (*domain.AcmeAccount, error) { diff --git a/internal/repository/certificate.go b/internal/repository/certificate.go index 5e1a7f8d..db0e2b4c 100644 --- a/internal/repository/certificate.go +++ b/internal/repository/certificate.go @@ -79,6 +79,51 @@ func (r *CertificateRepository) GetByWorkflowNodeId(ctx context.Context, workflo return r.castRecordToModel(records[0]) } +func (r *CertificateRepository) Save(ctx context.Context, certificate *domain.Certificate) (*domain.Certificate, error) { + collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameCertificate) + if err != nil { + return certificate, err + } + + var record *core.Record + if certificate.Id == "" { + record = core.NewRecord(collection) + } else { + record, err = app.GetApp().FindRecordById(collection, certificate.Id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return certificate, domain.ErrRecordNotFound + } + return certificate, err + } + } + + record.Set("source", string(certificate.Source)) + record.Set("subjectAltNames", certificate.SubjectAltNames) + record.Set("serialNumber", certificate.SerialNumber) + record.Set("certificate", certificate.Certificate) + record.Set("privateKey", certificate.PrivateKey) + record.Set("issuer", certificate.Issuer) + record.Set("issuerCertificate", certificate.IssuerCertificate) + record.Set("keyAlgorithm", string(certificate.KeyAlgorithm)) + record.Set("effectAt", certificate.EffectAt) + record.Set("expireAt", certificate.ExpireAt) + record.Set("acmeAccountUrl", certificate.ACMEAccountUrl) + record.Set("acmeCertUrl", certificate.ACMECertUrl) + record.Set("acmeCertStableUrl", certificate.ACMECertStableUrl) + record.Set("workflowId", certificate.WorkflowId) + record.Set("workflowNodeId", certificate.WorkflowNodeId) + record.Set("workflowOutputId", certificate.WorkflowOutputId) + if err := app.GetApp().Save(record); err != nil { + return certificate, err + } + + certificate.Id = record.Id + certificate.CreatedAt = record.GetDateTime("created").Time() + certificate.UpdatedAt = record.GetDateTime("updated").Time() + return certificate, nil +} + func (r *CertificateRepository) castRecordToModel(record *core.Record) (*domain.Certificate, error) { if record == nil { return nil, fmt.Errorf("record is nil") @@ -92,11 +137,15 @@ func (r *CertificateRepository) castRecordToModel(record *core.Record) (*domain. }, Source: domain.CertificateSourceType(record.GetString("source")), SubjectAltNames: record.GetString("subjectAltNames"), + SerialNumber: record.GetString("serialNumber"), Certificate: record.GetString("certificate"), PrivateKey: record.GetString("privateKey"), + Issuer: record.GetString("issuer"), IssuerCertificate: record.GetString("issuerCertificate"), + KeyAlgorithm: domain.CertificateKeyAlgorithmType(record.GetString("keyAlgorithm")), EffectAt: record.GetDateTime("effectAt").Time(), ExpireAt: record.GetDateTime("expireAt").Time(), + ACMEAccountUrl: record.GetString("acmeAccountUrl"), ACMECertUrl: record.GetString("acmeCertUrl"), ACMECertStableUrl: record.GetString("acmeCertStableUrl"), WorkflowId: record.GetString("workflowId"), diff --git a/internal/repository/workflow.go b/internal/repository/workflow.go index edf7cf7f..738e898e 100644 --- a/internal/repository/workflow.go +++ b/internal/repository/workflow.go @@ -65,7 +65,7 @@ func (r *WorkflowRepository) Save(ctx context.Context, workflow *domain.Workflow if workflow.Id == "" { record = core.NewRecord(collection) } else { - record, err = app.GetApp().FindRecordById(domain.CollectionNameWorkflow, workflow.Id) + record, err = app.GetApp().FindRecordById(collection, workflow.Id) if err != nil { if errors.Is(err, sql.ErrNoRows) { return workflow, domain.ErrRecordNotFound @@ -85,7 +85,6 @@ func (r *WorkflowRepository) Save(ctx context.Context, workflow *domain.Workflow record.Set("lastRunId", workflow.LastRunId) record.Set("lastRunStatus", string(workflow.LastRunStatus)) record.Set("lastRunTime", workflow.LastRunTime) - if err := app.GetApp().Save(record); err != nil { return workflow, err } @@ -96,63 +95,63 @@ func (r *WorkflowRepository) Save(ctx context.Context, workflow *domain.Workflow return workflow, nil } -func (r *WorkflowRepository) SaveRun(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error) { +func (r *WorkflowRepository) SaveRun(ctx context.Context, run *domain.WorkflowRun) (*domain.WorkflowRun, error) { collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflowRun) if err != nil { - return workflowRun, err + return run, err } - var workflowRunRecord *core.Record - if workflowRun.Id == "" { - workflowRunRecord = core.NewRecord(collection) + var runRecord *core.Record + if run.Id == "" { + runRecord = core.NewRecord(collection) } else { - workflowRunRecord, err = app.GetApp().FindRecordById(domain.CollectionNameWorkflowRun, workflowRun.Id) + runRecord, err = app.GetApp().FindRecordById(collection, run.Id) if err != nil { if errors.Is(err, sql.ErrNoRows) { - return workflowRun, err + return run, err } - workflowRunRecord = core.NewRecord(collection) + runRecord = core.NewRecord(collection) } } err = app.GetApp().RunInTransaction(func(txApp core.App) error { - workflowRunRecord.Set("workflowId", workflowRun.WorkflowId) - workflowRunRecord.Set("trigger", string(workflowRun.Trigger)) - workflowRunRecord.Set("status", string(workflowRun.Status)) - workflowRunRecord.Set("startedAt", workflowRun.StartedAt) - workflowRunRecord.Set("endedAt", workflowRun.EndedAt) - workflowRunRecord.Set("logs", workflowRun.Logs) - workflowRunRecord.Set("error", workflowRun.Error) - err = txApp.Save(workflowRunRecord) + runRecord.Set("workflowId", run.WorkflowId) + runRecord.Set("trigger", string(run.Trigger)) + runRecord.Set("status", string(run.Status)) + runRecord.Set("startedAt", run.StartedAt) + runRecord.Set("endedAt", run.EndedAt) + runRecord.Set("logs", run.Logs) + runRecord.Set("error", run.Error) + err = txApp.Save(runRecord) if err != nil { return err } - workflowRecord, err := txApp.FindRecordById(domain.CollectionNameWorkflow, workflowRun.WorkflowId) + workflowRecord, err := txApp.FindRecordById(domain.CollectionNameWorkflow, run.WorkflowId) if err != nil { return err } workflowRecord.IgnoreUnchangedFields(true) - workflowRecord.Set("lastRunId", workflowRunRecord.Id) - workflowRecord.Set("lastRunStatus", workflowRunRecord.GetString("status")) - workflowRecord.Set("lastRunTime", workflowRunRecord.GetString("startedAt")) + workflowRecord.Set("lastRunId", runRecord.Id) + workflowRecord.Set("lastRunStatus", runRecord.GetString("status")) + workflowRecord.Set("lastRunTime", runRecord.GetString("startedAt")) err = txApp.Save(workflowRecord) if err != nil { return err } - workflowRun.Id = workflowRunRecord.Id - workflowRun.CreatedAt = workflowRunRecord.GetDateTime("created").Time() - workflowRun.UpdatedAt = workflowRunRecord.GetDateTime("updated").Time() + run.Id = runRecord.Id + run.CreatedAt = runRecord.GetDateTime("created").Time() + run.UpdatedAt = runRecord.GetDateTime("updated").Time() return nil }) if err != nil { - return workflowRun, err + return run, err } - return workflowRun, nil + return run, nil } func (r *WorkflowRepository) castRecordToModel(record *core.Record) (*domain.Workflow, error) { diff --git a/internal/repository/workflow_output.go b/internal/repository/workflow_output.go index 724bb799..f5965396 100644 --- a/internal/repository/workflow_output.go +++ b/internal/repository/workflow_output.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "fmt" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" @@ -17,13 +18,13 @@ func NewWorkflowOutputRepository() *WorkflowOutputRepository { return &WorkflowOutputRepository{} } -func (r *WorkflowOutputRepository) GetByNodeId(ctx context.Context, nodeId string) (*domain.WorkflowOutput, error) { +func (r *WorkflowOutputRepository) GetByNodeId(ctx context.Context, workflowNodeId string) (*domain.WorkflowOutput, error) { records, err := app.GetApp().FindRecordsByFilter( domain.CollectionNameWorkflowOutput, "nodeId={:nodeId}", "-created", 1, 0, - dbx.Params{"nodeId": nodeId}, + dbx.Params{"nodeId": workflowNodeId}, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -34,7 +35,61 @@ func (r *WorkflowOutputRepository) GetByNodeId(ctx context.Context, nodeId strin if len(records) == 0 { return nil, domain.ErrRecordNotFound } - record := records[0] + + return r.castRecordToModel(records[0]) +} + +func (r *WorkflowOutputRepository) Save(ctx context.Context, workflowOutput *domain.WorkflowOutput) (*domain.WorkflowOutput, error) { + record, err := r.saveRecord(ctx, workflowOutput) + if err != nil { + return workflowOutput, err + } + + workflowOutput.Id = record.Id + workflowOutput.CreatedAt = record.GetDateTime("created").Time() + workflowOutput.UpdatedAt = record.GetDateTime("updated").Time() + return workflowOutput, nil +} + +func (r *WorkflowOutputRepository) SaveWithCertificate(ctx context.Context, workflowOutput *domain.WorkflowOutput, certificate *domain.Certificate) (*domain.WorkflowOutput, error) { + record, err := r.saveRecord(ctx, workflowOutput) + if err != nil { + return workflowOutput, err + } else { + workflowOutput.Id = record.Id + workflowOutput.CreatedAt = record.GetDateTime("created").Time() + workflowOutput.UpdatedAt = record.GetDateTime("updated").Time() + } + + if certificate != nil { + certificate.WorkflowId = workflowOutput.WorkflowId + certificate.WorkflowNodeId = workflowOutput.NodeId + certificate.WorkflowOutputId = workflowOutput.Id + certificate, err := NewCertificateRepository().Save(ctx, certificate) + if err != nil { + return workflowOutput, err + } + + // 写入证书 ID 到工作流输出结果中 + for i, item := range workflowOutput.Outputs { + if item.Name == string(domain.WorkflowNodeIONameCertificate) { + workflowOutput.Outputs[i].Value = certificate.Id + break + } + } + record.Set("outputs", workflowOutput.Outputs) + if err := app.GetApp().Save(record); err != nil { + return workflowOutput, err + } + } + + return workflowOutput, err +} + +func (r *WorkflowOutputRepository) castRecordToModel(record *core.Record) (*domain.WorkflowOutput, error) { + if record == nil { + return nil, fmt.Errorf("record is nil") + } node := &domain.WorkflowNode{} if err := record.UnmarshalJSONField("node", node); err != nil { @@ -46,7 +101,7 @@ func (r *WorkflowOutputRepository) GetByNodeId(ctx context.Context, nodeId strin return nil, errors.New("failed to unmarshal output") } - rs := &domain.WorkflowOutput{ + workflowOutput := &domain.WorkflowOutput{ Meta: domain.Meta{ Id: record.Id, CreatedAt: record.GetDateTime("created").Time(), @@ -58,25 +113,22 @@ func (r *WorkflowOutputRepository) GetByNodeId(ctx context.Context, nodeId strin Outputs: outputs, Succeeded: record.GetBool("succeeded"), } - - return rs, nil + return workflowOutput, nil } -// 保存节点输出 -func (r *WorkflowOutputRepository) Save(ctx context.Context, output *domain.WorkflowOutput, certificate *domain.Certificate, cb func(id string) error) error { - var record *core.Record - var err error +func (r *WorkflowOutputRepository) saveRecord(ctx context.Context, output *domain.WorkflowOutput) (*core.Record, error) { + collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflowOutput) + if err != nil { + return nil, err + } + var record *core.Record if output.Id == "" { - collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflowOutput) - if err != nil { - return err - } record = core.NewRecord(collection) } else { - record, err = app.GetApp().FindRecordById(domain.CollectionNameWorkflowOutput, output.Id) + record, err = app.GetApp().FindRecordById(collection, output.Id) if err != nil { - return err + return record, err } } record.Set("workflowId", output.WorkflowId) @@ -84,53 +136,9 @@ func (r *WorkflowOutputRepository) Save(ctx context.Context, output *domain.Work record.Set("node", output.Node) record.Set("outputs", output.Outputs) record.Set("succeeded", output.Succeeded) - if err := app.GetApp().Save(record); err != nil { - return err + return record, err } - if cb != nil && certificate != nil { - if err := cb(record.Id); err != nil { - return err - } - - certCollection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameCertificate) - if err != nil { - return err - } - - certRecord := core.NewRecord(certCollection) - certRecord.Set("source", string(certificate.Source)) - certRecord.Set("subjectAltNames", certificate.SubjectAltNames) - certRecord.Set("certificate", certificate.Certificate) - certRecord.Set("privateKey", certificate.PrivateKey) - certRecord.Set("issuerCertificate", certificate.IssuerCertificate) - certRecord.Set("effectAt", certificate.EffectAt) - certRecord.Set("expireAt", certificate.ExpireAt) - certRecord.Set("acmeCertUrl", certificate.ACMECertUrl) - certRecord.Set("acmeCertStableUrl", certificate.ACMECertStableUrl) - certRecord.Set("workflowId", certificate.WorkflowId) - certRecord.Set("workflowNodeId", certificate.WorkflowNodeId) - certRecord.Set("workflowOutputId", certificate.WorkflowOutputId) - - if err := app.GetApp().Save(certRecord); err != nil { - return err - } - - // 更新 certificate - for i, item := range output.Outputs { - if item.Name == string(domain.WorkflowNodeIONameCertificate) { - output.Outputs[i].Value = certRecord.Id - break - } - } - - record.Set("outputs", output.Outputs) - - if err := app.GetApp().Save(record); err != nil { - return err - } - - } - return nil + return record, err } diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index 5ca379c4..76f180f0 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -3,7 +3,6 @@ package nodeprocessor import ( "context" "fmt" - "strings" "time" "golang.org/x/exp/maps" @@ -30,89 +29,79 @@ func NewApplyNode(node *domain.WorkflowNode) *applyNode { } } -// 申请节点根据申请类型执行不同的操作 -func (a *applyNode) Run(ctx context.Context) error { - a.AddOutput(ctx, a.node.Name, "开始执行") +func (n *applyNode) Run(ctx context.Context) error { + n.AddOutput(ctx, n.node.Name, "开始执行") // 查询上次执行结果 - lastOutput, err := a.outputRepo.GetByNodeId(ctx, a.node.Id) + lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id) if err != nil && !domain.IsRecordNotFoundError(err) { - a.AddOutput(ctx, a.node.Name, "查询申请记录失败", err.Error()) + n.AddOutput(ctx, n.node.Name, "查询申请记录失败", err.Error()) return err } // 检测是否可以跳过本次执行 - if skippable, skipReason := a.checkCanSkip(ctx, lastOutput); skippable { - a.AddOutput(ctx, a.node.Name, skipReason) + if skippable, skipReason := n.checkCanSkip(ctx, lastOutput); skippable { + n.AddOutput(ctx, n.node.Name, skipReason) return nil } // 初始化申请器 - applicant, err := applicant.NewWithApplyNode(a.node) + applicant, err := applicant.NewWithApplyNode(n.node) if err != nil { - a.AddOutput(ctx, a.node.Name, "获取申请对象失败", err.Error()) + n.AddOutput(ctx, n.node.Name, "获取申请对象失败", err.Error()) return err } // 申请证书 applyResult, err := applicant.Apply() if err != nil { - a.AddOutput(ctx, a.node.Name, "申请失败", err.Error()) + n.AddOutput(ctx, n.node.Name, "申请失败", err.Error()) return err } - a.AddOutput(ctx, a.node.Name, "申请成功") + n.AddOutput(ctx, n.node.Name, "申请成功") // 解析证书并生成实体 certX509, err := certs.ParseCertificateFromPEM(applyResult.CertificateFullChain) if err != nil { - a.AddOutput(ctx, a.node.Name, "解析证书失败", err.Error()) + n.AddOutput(ctx, n.node.Name, "解析证书失败", err.Error()) return err } certificate := &domain.Certificate{ Source: domain.CertificateSourceTypeWorkflow, - SubjectAltNames: strings.Join(certX509.DNSNames, ";"), Certificate: applyResult.CertificateFullChain, PrivateKey: applyResult.PrivateKey, IssuerCertificate: applyResult.IssuerCertificate, + ACMEAccountUrl: applyResult.ACMEAccountUrl, ACMECertUrl: applyResult.ACMECertUrl, ACMECertStableUrl: applyResult.ACMECertStableUrl, - EffectAt: certX509.NotBefore, - ExpireAt: certX509.NotAfter, - WorkflowId: getContextWorkflowId(ctx), - WorkflowNodeId: a.node.Id, } + certificate.PopulateFromX509(certX509) // 保存执行结果 // TODO: 先保持一个节点始终只有一个输出,后续增加版本控制 currentOutput := &domain.WorkflowOutput{ WorkflowId: getContextWorkflowId(ctx), - NodeId: a.node.Id, - Node: a.node, + NodeId: n.node.Id, + Node: n.node, Succeeded: true, - Outputs: a.node.Outputs, + Outputs: n.node.Outputs, } if lastOutput != nil { currentOutput.Id = lastOutput.Id } - if err := a.outputRepo.Save(ctx, currentOutput, certificate, func(id string) error { - if certificate != nil { - certificate.WorkflowOutputId = id - } - - return nil - }); err != nil { - a.AddOutput(ctx, a.node.Name, "保存申请记录失败", err.Error()) + if _, err := n.outputRepo.SaveWithCertificate(ctx, currentOutput, certificate); err != nil { + n.AddOutput(ctx, n.node.Name, "保存申请记录失败", err.Error()) return err } - a.AddOutput(ctx, a.node.Name, "保存申请记录成功") + n.AddOutput(ctx, n.node.Name, "保存申请记录成功") return nil } -func (a *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (skip bool, reason string) { +func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (skip bool, reason string) { if lastOutput != nil && lastOutput.Succeeded { // 比较和上次申请时的关键配置(即影响证书签发的)参数是否一致 - currentNodeConfig := a.node.GetConfigForApply() + currentNodeConfig := n.node.GetConfigForApply() lastNodeConfig := lastOutput.Node.GetConfigForApply() if currentNodeConfig.Domains != lastNodeConfig.Domains { return false, "配置项变化:域名" @@ -130,7 +119,7 @@ func (a *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo return false, "配置项变化:数字签名算法" } - lastCertificate, _ := a.certRepo.GetByWorkflowNodeId(ctx, a.node.Id) + lastCertificate, _ := n.certRepo.GetByWorkflowNodeId(ctx, n.node.Id) renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24 expirationTime := time.Until(lastCertificate.ExpireAt) if lastCertificate != nil && expirationTime > renewalInterval { diff --git a/internal/workflow/node-processor/condition_node.go b/internal/workflow/node-processor/condition_node.go index cd3ab07f..a511ce20 100644 --- a/internal/workflow/node-processor/condition_node.go +++ b/internal/workflow/node-processor/condition_node.go @@ -18,11 +18,9 @@ func NewConditionNode(node *domain.WorkflowNode) *conditionNode { } } -// 条件节点没有任何操作 -func (c *conditionNode) Run(ctx context.Context) error { - c.AddOutput(ctx, - c.node.Name, - "完成", - ) +func (n *conditionNode) Run(ctx context.Context) error { + // 此类型节点不需要执行任何操作,直接返回 + n.AddOutput(ctx, n.node.Name, "完成") + return nil } diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go index 48e344ad..28acadb0 100644 --- a/internal/workflow/node-processor/deploy_node.go +++ b/internal/workflow/node-processor/deploy_node.go @@ -27,81 +27,81 @@ func NewDeployNode(node *domain.WorkflowNode) *deployNode { } } -func (d *deployNode) Run(ctx context.Context) error { - d.AddOutput(ctx, d.node.Name, "开始执行") +func (n *deployNode) Run(ctx context.Context) error { + n.AddOutput(ctx, n.node.Name, "开始执行") // 查询上次执行结果 - lastOutput, err := d.outputRepo.GetByNodeId(ctx, d.node.Id) + lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id) if err != nil && !domain.IsRecordNotFoundError(err) { - d.AddOutput(ctx, d.node.Name, "查询部署记录失败", err.Error()) + n.AddOutput(ctx, n.node.Name, "查询部署记录失败", err.Error()) return err } // 获取前序节点输出证书 - previousNodeOutputCertificateSource := d.node.GetConfigForDeploy().Certificate + previousNodeOutputCertificateSource := n.node.GetConfigForDeploy().Certificate previousNodeOutputCertificateSourceSlice := strings.Split(previousNodeOutputCertificateSource, "#") if len(previousNodeOutputCertificateSourceSlice) != 2 { - d.AddOutput(ctx, d.node.Name, "证书来源配置错误", previousNodeOutputCertificateSource) + n.AddOutput(ctx, n.node.Name, "证书来源配置错误", previousNodeOutputCertificateSource) return fmt.Errorf("证书来源配置错误: %s", previousNodeOutputCertificateSource) } - certificate, err := d.certRepo.GetByWorkflowNodeId(ctx, previousNodeOutputCertificateSourceSlice[0]) + certificate, err := n.certRepo.GetByWorkflowNodeId(ctx, previousNodeOutputCertificateSourceSlice[0]) if err != nil { - d.AddOutput(ctx, d.node.Name, "获取证书失败", err.Error()) + n.AddOutput(ctx, n.node.Name, "获取证书失败", err.Error()) return err } // 检测是否可以跳过本次执行 - if skippable, skipReason := d.checkCanSkip(ctx, lastOutput); skippable { + if skippable, skipReason := n.checkCanSkip(ctx, lastOutput); skippable { if certificate.CreatedAt.Before(lastOutput.UpdatedAt) { - d.AddOutput(ctx, d.node.Name, "已部署过且证书未更新") + n.AddOutput(ctx, n.node.Name, "已部署过且证书未更新") } else { - d.AddOutput(ctx, d.node.Name, skipReason) + n.AddOutput(ctx, n.node.Name, skipReason) } return nil } // 初始化部署器 - deploy, err := deployer.NewWithDeployNode(d.node, struct { + deploy, err := deployer.NewWithDeployNode(n.node, struct { Certificate string PrivateKey string }{Certificate: certificate.Certificate, PrivateKey: certificate.PrivateKey}) if err != nil { - d.AddOutput(ctx, d.node.Name, "获取部署对象失败", err.Error()) + n.AddOutput(ctx, n.node.Name, "获取部署对象失败", err.Error()) return err } // 部署证书 if err := deploy.Deploy(ctx); err != nil { - d.AddOutput(ctx, d.node.Name, "部署失败", err.Error()) + n.AddOutput(ctx, n.node.Name, "部署失败", err.Error()) return err } - d.AddOutput(ctx, d.node.Name, "部署成功") + n.AddOutput(ctx, n.node.Name, "部署成功") // 保存执行结果 // TODO: 先保持一个节点始终只有一个输出,后续增加版本控制 currentOutput := &domain.WorkflowOutput{ Meta: domain.Meta{}, WorkflowId: getContextWorkflowId(ctx), - NodeId: d.node.Id, - Node: d.node, + NodeId: n.node.Id, + Node: n.node, Succeeded: true, } if lastOutput != nil { currentOutput.Id = lastOutput.Id } - if err := d.outputRepo.Save(ctx, currentOutput, nil, nil); err != nil { - d.AddOutput(ctx, d.node.Name, "保存部署记录失败", err.Error()) + if _, err := n.outputRepo.Save(ctx, currentOutput); err != nil { + n.AddOutput(ctx, n.node.Name, "保存部署记录失败", err.Error()) return err } - d.AddOutput(ctx, d.node.Name, "保存部署记录成功") + n.AddOutput(ctx, n.node.Name, "保存部署记录成功") return nil } -func (d *deployNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (skip bool, reason string) { +func (n *deployNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (skip bool, reason string) { if lastOutput != nil && lastOutput.Succeeded { // 比较和上次部署时的关键配置(即影响证书部署的)参数是否一致 - currentNodeConfig := d.node.GetConfigForDeploy() + currentNodeConfig := n.node.GetConfigForDeploy() lastNodeConfig := lastOutput.Node.GetConfigForDeploy() if currentNodeConfig.ProviderAccessId != lastNodeConfig.ProviderAccessId { return false, "配置项变化:主机提供商授权" diff --git a/internal/workflow/node-processor/execute_failure_node.go b/internal/workflow/node-processor/execute_failure_node.go index d1ff0034..84042a4b 100644 --- a/internal/workflow/node-processor/execute_failure_node.go +++ b/internal/workflow/node-processor/execute_failure_node.go @@ -18,10 +18,9 @@ func NewExecuteFailureNode(node *domain.WorkflowNode) *executeFailureNode { } } -func (e *executeFailureNode) Run(ctx context.Context) error { - e.AddOutput(ctx, - e.node.Name, - "进入执行失败分支", - ) +func (n *executeFailureNode) Run(ctx context.Context) error { + // 此类型节点不需要执行任何操作,直接返回 + n.AddOutput(ctx, n.node.Name, "进入执行失败分支") + return nil } diff --git a/internal/workflow/node-processor/execute_success_node.go b/internal/workflow/node-processor/execute_success_node.go index d8d4139f..ef058b06 100644 --- a/internal/workflow/node-processor/execute_success_node.go +++ b/internal/workflow/node-processor/execute_success_node.go @@ -18,10 +18,9 @@ func NewExecuteSuccessNode(node *domain.WorkflowNode) *executeSuccessNode { } } -func (e *executeSuccessNode) Run(ctx context.Context) error { - e.AddOutput(ctx, - e.node.Name, - "进入执行成功分支", - ) +func (n *executeSuccessNode) Run(ctx context.Context) error { + // 此类型节点不需要执行任何操作,直接返回 + n.AddOutput(ctx, n.node.Name, "进入执行成功分支") + return nil } diff --git a/internal/workflow/node-processor/processor.go b/internal/workflow/node-processor/processor.go index 55e8477a..bf2b12f4 100644 --- a/internal/workflow/node-processor/processor.go +++ b/internal/workflow/node-processor/processor.go @@ -23,8 +23,9 @@ type certificateRepository interface { } type workflowOutputRepository interface { - GetByNodeId(ctx context.Context, nodeId string) (*domain.WorkflowOutput, error) - Save(ctx context.Context, output *domain.WorkflowOutput, certificate *domain.Certificate, cb func(id string) error) error + GetByNodeId(ctx context.Context, workflowNodeId string) (*domain.WorkflowOutput, error) + Save(ctx context.Context, workflowOutput *domain.WorkflowOutput) (*domain.WorkflowOutput, error) + SaveWithCertificate(ctx context.Context, workflowOutput *domain.WorkflowOutput, certificate *domain.Certificate) (*domain.WorkflowOutput, error) } type settingsRepository interface { diff --git a/internal/workflow/node-processor/start_node.go b/internal/workflow/node-processor/start_node.go index 81d93de6..2f1026ad 100644 --- a/internal/workflow/node-processor/start_node.go +++ b/internal/workflow/node-processor/start_node.go @@ -18,9 +18,9 @@ func NewStartNode(node *domain.WorkflowNode) *startNode { } } -func (s *startNode) Run(ctx context.Context) error { - // 开始节点没有任何操作 - s.AddOutput(ctx, s.node.Name, "完成") +func (n *startNode) Run(ctx context.Context) error { + // 此类型节点不需要执行任何操作,直接返回 + n.AddOutput(ctx, n.node.Name, "完成") return nil } diff --git a/internal/workflow/node-processor/upload_node.go b/internal/workflow/node-processor/upload_node.go index 8aa0bba7..7b1908d9 100644 --- a/internal/workflow/node-processor/upload_node.go +++ b/internal/workflow/node-processor/upload_node.go @@ -3,7 +3,6 @@ package nodeprocessor import ( "context" "errors" - "strings" "time" "github.com/usual2970/certimate/internal/domain" @@ -28,43 +27,34 @@ func NewUploadNode(node *domain.WorkflowNode) *uploadNode { // Run 上传证书节点执行 // 包含上传证书的工作流,理论上应该手动执行,如果每天定时执行,也只是重新保存一下 func (n *uploadNode) Run(ctx context.Context) error { - n.AddOutput(ctx, - n.node.Name, - "进入上传证书节点", - ) + n.AddOutput(ctx, n.node.Name, "进入上传证书节点") - config := n.node.GetConfigForUpload() + nodeConfig := n.node.GetConfigForUpload() - // 检查证书是否过期 - // 如果证书过期,则直接返回错误 - certX509, err := certs.ParseCertificateFromPEM(config.Certificate) - if err != nil { - n.AddOutput(ctx, - n.node.Name, - "解析证书失败", - ) + // 查询上次执行结果 + lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id) + if err != nil && !domain.IsRecordNotFoundError(err) { + n.AddOutput(ctx, n.node.Name, "查询申请记录失败", err.Error()) return err } + // 检查证书是否过期 + // 如果证书过期,则直接返回错误 + certX509, err := certs.ParseCertificateFromPEM(nodeConfig.Certificate) + if err != nil { + n.AddOutput(ctx, n.node.Name, "解析证书失败") + return err + } if time.Now().After(certX509.NotAfter) { - n.AddOutput(ctx, - n.node.Name, - "证书已过期", - ) + n.AddOutput(ctx, n.node.Name, "证书已过期") return errors.New("certificate is expired") } + // 生成实体 certificate := &domain.Certificate{ - Source: domain.CertificateSourceTypeUpload, - SubjectAltNames: strings.Join(certX509.DNSNames, ";"), - Certificate: config.Certificate, - PrivateKey: config.PrivateKey, - - EffectAt: certX509.NotBefore, - ExpireAt: certX509.NotAfter, - WorkflowId: getContextWorkflowId(ctx), - WorkflowNodeId: n.node.Id, + Source: domain.CertificateSourceTypeUpload, } + certificate.PopulateFromPEM(nodeConfig.Certificate, nodeConfig.PrivateKey) // 保存执行结果 // TODO: 先保持一个节点始终只有一个输出,后续增加版本控制 @@ -75,23 +65,10 @@ func (n *uploadNode) Run(ctx context.Context) error { Succeeded: true, Outputs: n.node.Outputs, } - - // 查询上次执行结果 - lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id) - if err != nil && !domain.IsRecordNotFoundError(err) { - n.AddOutput(ctx, n.node.Name, "查询上传记录失败", err.Error()) - return err - } if lastOutput != nil { currentOutput.Id = lastOutput.Id } - if err := n.outputRepo.Save(ctx, currentOutput, certificate, func(id string) error { - if certificate != nil { - certificate.WorkflowOutputId = id - } - - return nil - }); err != nil { + if _, err := n.outputRepo.SaveWithCertificate(ctx, currentOutput, certificate); err != nil { n.AddOutput(ctx, n.node.Name, "保存上传记录失败", err.Error()) return err } diff --git a/migrations/1738767422_updated_certificate.go b/migrations/1738767422_updated_certificate.go new file mode 100644 index 00000000..e5dfe573 --- /dev/null +++ b/migrations/1738767422_updated_certificate.go @@ -0,0 +1,127 @@ +package migrations + +import ( + "encoding/json" + + "github.com/pocketbase/pocketbase/core" + m "github.com/pocketbase/pocketbase/migrations" +) + +func init() { + m.Register(func(app core.App) error { + collection, err := app.FindCollectionByNameOrId("4szxr9x43tpj6np") + if err != nil { + return err + } + + // update collection data + if err := json.Unmarshal([]byte(`{ + "indexes": [ + "CREATE INDEX ` + "`" + `idx_Jx8TXzDCmw` + "`" + ` ON ` + "`" + `certificate` + "`" + ` (` + "`" + `workflowId` + "`" + `)", + "CREATE INDEX ` + "`" + `idx_kcKpgAZapk` + "`" + ` ON ` + "`" + `certificate` + "`" + ` (` + "`" + `workflowNodeId` + "`" + `)" + ] + }`), &collection); err != nil { + return err + } + + // add field + if err := collection.Fields.AddMarshaledJSONAt(3, []byte(`{ + "autogeneratePattern": "", + "hidden": false, + "id": "text2069360702", + "max": 0, + "min": 0, + "name": "serialNumber", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }`)); err != nil { + return err + } + + // add field + if err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{ + "autogeneratePattern": "", + "hidden": false, + "id": "text2910474005", + "max": 0, + "min": 0, + "name": "issuer", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }`)); err != nil { + return err + } + + // add field + if err := collection.Fields.AddMarshaledJSONAt(8, []byte(`{ + "autogeneratePattern": "", + "hidden": false, + "id": "text4164403445", + "max": 0, + "min": 0, + "name": "keyAlgorithm", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }`)); err != nil { + return err + } + + // add field + if err := collection.Fields.AddMarshaledJSONAt(11, []byte(`{ + "autogeneratePattern": "", + "hidden": false, + "id": "text2045248758", + "max": 0, + "min": 0, + "name": "acmeAccountUrl", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }`)); err != nil { + return err + } + + return app.Save(collection) + }, func(app core.App) error { + collection, err := app.FindCollectionByNameOrId("4szxr9x43tpj6np") + if err != nil { + return err + } + + // update collection data + if err := json.Unmarshal([]byte(`{ + "indexes": [] + }`), &collection); err != nil { + return err + } + + // remove field + collection.Fields.RemoveById("text2069360702") + + // remove field + collection.Fields.RemoveById("text2910474005") + + // remove field + collection.Fields.RemoveById("text4164403445") + + // remove field + collection.Fields.RemoveById("text2045248758") + + return app.Save(collection) + }) +} diff --git a/migrations/1737479489_updated_workflow.go b/migrations/1738828775_updated_workflow.go similarity index 100% rename from migrations/1737479489_updated_workflow.go rename to migrations/1738828775_updated_workflow.go diff --git a/migrations/1737479538_updated_workflow_run.go b/migrations/1738828788_updated_workflow_run.go similarity index 100% rename from migrations/1737479538_updated_workflow_run.go rename to migrations/1738828788_updated_workflow_run.go diff --git a/ui/src/components/certificate/CertificateDetail.tsx b/ui/src/components/certificate/CertificateDetail.tsx index 6feb992b..d57a9609 100644 --- a/ui/src/components/certificate/CertificateDetail.tsx +++ b/ui/src/components/certificate/CertificateDetail.tsx @@ -38,11 +38,27 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
- + + + + + - + + + + + + + + + @@ -59,7 +75,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => { - + @@ -76,7 +92,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => { - +
diff --git a/ui/src/components/workflow/node/UploadNodeConfigForm.tsx b/ui/src/components/workflow/node/UploadNodeConfigForm.tsx index bc354eb4..807edb11 100644 --- a/ui/src/components/workflow/node/UploadNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/UploadNodeConfigForm.tsx @@ -137,7 +137,7 @@ const UploadNodeConfigForm = forwardRef - + diff --git a/ui/src/domain/certificate.ts b/ui/src/domain/certificate.ts index 2ec857fc..563d2476 100644 --- a/ui/src/domain/certificate.ts +++ b/ui/src/domain/certificate.ts @@ -3,8 +3,11 @@ import { type WorkflowModel } from "./workflow"; export interface CertificateModel extends BaseModel { source: string; subjectAltNames: string; + serialNumber: string; certificate: string; privateKey: string; + issuer: string; + keyAlgorithm: string; effectAt: ISO8601String; expireAt: ISO8601String; workflowId: string; diff --git a/ui/src/i18n/locales/en/nls.certificate.json b/ui/src/i18n/locales/en/nls.certificate.json index deb6508d..7557daff 100644 --- a/ui/src/i18n/locales/en/nls.certificate.json +++ b/ui/src/i18n/locales/en/nls.certificate.json @@ -15,11 +15,15 @@ "certificate.props.validity.expiration": "Expire on {{date}}", "certificate.props.validity.filter.expire_soon": "Expire soon", "certificate.props.validity.filter.expired": "Expired", + "certificate.props.brand": "Brand", "certificate.props.source": "Source", "certificate.props.source.workflow": "Workflow", "certificate.props.source.upload": "Upload", "certificate.props.certificate": "Certificate chain", "certificate.props.private_key": "Private key", + "certificate.props.serial_number": "Serial number", + "certificate.props.key_algorithm": "Key algorithm", + "certificate.props.issuer": "Issuer", "certificate.props.created_at": "Created at", "certificate.props.updated_at": "Updated at" } diff --git a/ui/src/i18n/locales/zh/nls.certificate.json b/ui/src/i18n/locales/zh/nls.certificate.json index f4a86d95..b4e9bee0 100644 --- a/ui/src/i18n/locales/zh/nls.certificate.json +++ b/ui/src/i18n/locales/zh/nls.certificate.json @@ -15,11 +15,15 @@ "certificate.props.validity.expiration": "{{date}} 到期", "certificate.props.validity.filter.expire_soon": "即将到期", "certificate.props.validity.filter.expired": "已到期", + "certificate.props.brand": "证书品牌", "certificate.props.source": "来源", "certificate.props.source.workflow": "工作流", "certificate.props.source.upload": "用户上传", "certificate.props.certificate": "证书内容", "certificate.props.private_key": "私钥内容", + "certificate.props.serial_number": "证书序列号", + "certificate.props.key_algorithm": "证书算法", + "certificate.props.issuer": "颁发者", "certificate.props.created_at": "创建时间", "certificate.props.updated_at": "更新时间" } diff --git a/ui/src/pages/certificates/CertificateList.tsx b/ui/src/pages/certificates/CertificateList.tsx index 3c7aa98f..63a03f6d 100644 --- a/ui/src/pages/certificates/CertificateList.tsx +++ b/ui/src/pages/certificates/CertificateList.tsx @@ -107,6 +107,16 @@ const CertificateList = () => { ); }, }, + { + key: "issuer", + title: t("certificate.props.brand"), + render: (_, record) => ( + + {record.issuer} + {record.keyAlgorithm} + + ), + }, { key: "source", title: t("certificate.props.source"), @@ -250,7 +260,7 @@ const CertificateList = () => { dataSource={tableData} loading={loading} locale={{ - emptyText: , + emptyText: , }} pagination={{ current: page, diff --git a/ui/src/pages/workflows/WorkflowDetail.tsx b/ui/src/pages/workflows/WorkflowDetail.tsx index f85394d1..4a2f8022 100644 --- a/ui/src/pages/workflows/WorkflowDetail.tsx +++ b/ui/src/pages/workflows/WorkflowDetail.tsx @@ -60,13 +60,13 @@ const WorkflowDetail = () => { const [allowRun, setAllowRun] = useState(false); useEffect(() => { - setIsRunning(lastRunStatus == WORKFLOW_RUN_STATUSES.RUNNING); + setIsRunning(lastRunStatus == WORKFLOW_RUN_STATUSES.PENDING || lastRunStatus == WORKFLOW_RUN_STATUSES.RUNNING); }, [lastRunStatus]); useEffect(() => { if (!!workflowId && isRunning) { subscribeWorkflow(workflowId, (e) => { - if (e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.RUNNING) { + if (e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.PENDING && e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.RUNNING) { setIsRunning(false); unsubscribeWorkflow(workflowId); } @@ -178,7 +178,7 @@ const WorkflowDetail = () => { // subscribe before running workflow unsubscribeFn = await subscribeWorkflow(workflowId!, (e) => { - if (e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.RUNNING) { + if (e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.PENDING && e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.RUNNING) { setIsRunning(false); unsubscribeFn?.(); } diff --git a/ui/src/pages/workflows/WorkflowList.tsx b/ui/src/pages/workflows/WorkflowList.tsx index 28b776e7..29dad70d 100644 --- a/ui/src/pages/workflows/WorkflowList.tsx +++ b/ui/src/pages/workflows/WorkflowList.tsx @@ -350,7 +350,7 @@ const WorkflowList = () => { dataSource={tableData} loading={loading} locale={{ - emptyText: , + emptyText: , }} pagination={{ current: page, diff --git a/ui/src/utils/error.ts b/ui/src/utils/error.ts index df5465f4..8ffb047a 100644 --- a/ui/src/utils/error.ts +++ b/ui/src/utils/error.ts @@ -7,13 +7,13 @@ export const getErrMsg = (error: unknown): string => { return error.message; } else if (typeof error === "object" && error != null) { if ("message" in error) { - return String(error.message); + return getErrMsg(error.message); } else if ("msg" in error) { - return String(error.msg); + return getErrMsg(error.msg); } } else if (typeof error === "string") { - return error; + return error || "Unknown error"; } - return String(error ?? "Unknown error"); + return "Unknown error"; };