From dea4106569a6faa26f056bb1132d52773ade6925 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 16 Jan 2025 16:47:33 +0800 Subject: [PATCH 1/8] fix: couldn't return stdout or stderr during script execution if errors occur on deployment to local/ssh --- .../core/deployer/providers/local/local.go | 17 ++++----- .../deployer/providers/local/local_test.go | 38 ++++++++++++------- .../pkg/core/deployer/providers/ssh/ssh.go | 14 +++---- .../core/deployer/providers/ssh/ssh_test.go | 1 + 4 files changed, 41 insertions(+), 29 deletions(-) diff --git a/internal/pkg/core/deployer/providers/local/local.go b/internal/pkg/core/deployer/providers/local/local.go index e3c3d84e..e254caf9 100644 --- a/internal/pkg/core/deployer/providers/local/local.go +++ b/internal/pkg/core/deployer/providers/local/local.go @@ -75,7 +75,7 @@ func (d *LocalDeployer) Deploy(ctx context.Context, certPem string, privkeyPem s 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) + return nil, xerrors.Wrapf(err, "failed to execute pre-command, stdout: %s, stderr: %s", stdout, stderr) } d.logger.Logt("pre-command executed", stdout) @@ -132,7 +132,7 @@ func (d *LocalDeployer) Deploy(ctx context.Context, certPem string, privkeyPem s 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) + return nil, xerrors.Wrapf(err, "failed to execute post-command, stdout: %s, stderr: %s", stdout, stderr) } d.logger.Logt("post-command executed", stdout) @@ -154,7 +154,7 @@ func execCommand(shellEnv ShellEnvType, command string) (string, string, error) case SHELL_ENV_POWERSHELL: cmd = exec.Command("powershell", "-Command", command) - case "": + case ShellEnvType(""): if runtime.GOOS == "windows" { cmd = exec.Command("cmd", "/C", command) } else { @@ -165,14 +165,13 @@ func execCommand(shellEnv ShellEnvType, command string) (string, string, error) return "", "", fmt.Errorf("unsupported shell env: %s", shellEnv) } - var stdoutBuf bytes.Buffer - cmd.Stdout = &stdoutBuf - var stderrBuf bytes.Buffer - cmd.Stderr = &stderrBuf - + stdoutBuf := bytes.NewBuffer(nil) + cmd.Stdout = stdoutBuf + stderrBuf := bytes.NewBuffer(nil) + cmd.Stderr = stderrBuf err := cmd.Run() if err != nil { - return "", "", xerrors.Wrap(err, "failed to execute shell command") + return stdoutBuf.String(), stderrBuf.String(), xerrors.Wrap(err, "failed to execute command") } return stdoutBuf.String(), stderrBuf.String(), nil diff --git a/internal/pkg/core/deployer/providers/local/local_test.go b/internal/pkg/core/deployer/providers/local/local_test.go index 33ff4def..723ddf58 100644 --- a/internal/pkg/core/deployer/providers/local/local_test.go +++ b/internal/pkg/core/deployer/providers/local/local_test.go @@ -20,6 +20,9 @@ var ( fJksAlias string fJksKeypass string fJksStorepass string + fShellEnv string + fPreCommand string + fPostCommand string ) func init() { @@ -33,6 +36,9 @@ func init() { flag.StringVar(&fJksAlias, argsPrefix+"JKSALIAS", "", "") flag.StringVar(&fJksKeypass, argsPrefix+"JKSKEYPASS", "", "") flag.StringVar(&fJksStorepass, argsPrefix+"JKSSTOREPASS", "", "") + flag.StringVar(&fShellEnv, argsPrefix+"SHELLENV", "", "") + flag.StringVar(&fPreCommand, argsPrefix+"PRECOMMAND", "", "") + flag.StringVar(&fPostCommand, argsPrefix+"POSTCOMMAND", "", "") } /* @@ -46,7 +52,10 @@ Shell command to run this test: --CERTIMATE_DEPLOYER_LOCAL_PFXPASSWORD="your-pfx-password" \ --CERTIMATE_DEPLOYER_LOCAL_JKSALIAS="your-jks-alias" \ --CERTIMATE_DEPLOYER_LOCAL_JKSKEYPASS="your-jks-keypass" \ - --CERTIMATE_DEPLOYER_LOCAL_JKSSTOREPASS="your-jks-storepass" + --CERTIMATE_DEPLOYER_LOCAL_JKSSTOREPASS="your-jks-storepass" \ + --CERTIMATE_DEPLOYER_LOCAL_SHELLENV="sh" \ + --CERTIMATE_DEPLOYER_LOCAL_PRECOMMAND="echo 'hello world'" \ + --CERTIMATE_DEPLOYER_LOCAL_POSTCOMMAND="echo 'bye-bye world'" */ func TestDeploy(t *testing.T) { flag.Parse() @@ -58,11 +67,18 @@ func TestDeploy(t *testing.T) { fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("OUTPUTCERTPATH: %v", fOutputCertPath), fmt.Sprintf("OUTPUTKEYPATH: %v", fOutputKeyPath), + fmt.Sprintf("SHELLENV: %v", fShellEnv), + fmt.Sprintf("PRECOMMAND: %v", fPreCommand), + fmt.Sprintf("POSTCOMMAND: %v", fPostCommand), }, "\n")) deployer, err := provider.New(&provider.LocalDeployerConfig{ - OutputCertPath: fOutputCertPath, - OutputKeyPath: fOutputKeyPath, + OutputFormat: provider.OUTPUT_FORMAT_PEM, + OutputCertPath: fOutputCertPath + ".pem", + OutputKeyPath: fOutputKeyPath + ".pem", + ShellEnv: provider.ShellEnvType(fShellEnv), + PreCommand: fPreCommand, + PostCommand: fPostCommand, }) if err != nil { t.Errorf("err: %+v", err) @@ -77,7 +93,7 @@ func TestDeploy(t *testing.T) { return } - fstat1, err := os.Stat(fOutputCertPath) + fstat1, err := os.Stat(fOutputCertPath + ".pem") if err != nil { t.Errorf("err: %+v", err) return @@ -86,7 +102,7 @@ func TestDeploy(t *testing.T) { return } - fstat2, err := os.Stat(fOutputKeyPath) + fstat2, err := os.Stat(fOutputKeyPath + ".pem") if err != nil { t.Errorf("err: %+v", err) return @@ -104,14 +120,12 @@ func TestDeploy(t *testing.T) { fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("OUTPUTCERTPATH: %v", fOutputCertPath), - fmt.Sprintf("OUTPUTKEYPATH: %v", fOutputKeyPath), fmt.Sprintf("PFXPASSWORD: %v", fPfxPassword), }, "\n")) deployer, err := provider.New(&provider.LocalDeployerConfig{ OutputFormat: provider.OUTPUT_FORMAT_PFX, - OutputCertPath: fOutputCertPath, - OutputKeyPath: fOutputKeyPath, + OutputCertPath: fOutputCertPath + ".pfx", PfxPassword: fPfxPassword, }) if err != nil { @@ -127,7 +141,7 @@ func TestDeploy(t *testing.T) { return } - fstat, err := os.Stat(fOutputCertPath) + fstat, err := os.Stat(fOutputCertPath + ".pfx") if err != nil { t.Errorf("err: %+v", err) return @@ -145,7 +159,6 @@ func TestDeploy(t *testing.T) { fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("OUTPUTCERTPATH: %v", fOutputCertPath), - fmt.Sprintf("OUTPUTKEYPATH: %v", fOutputKeyPath), fmt.Sprintf("JKSALIAS: %v", fJksAlias), fmt.Sprintf("JKSKEYPASS: %v", fJksKeypass), fmt.Sprintf("JKSSTOREPASS: %v", fJksStorepass), @@ -153,8 +166,7 @@ func TestDeploy(t *testing.T) { deployer, err := provider.New(&provider.LocalDeployerConfig{ OutputFormat: provider.OUTPUT_FORMAT_JKS, - OutputCertPath: fOutputCertPath, - OutputKeyPath: fOutputKeyPath, + OutputCertPath: fOutputCertPath + ".jks", JksAlias: fJksAlias, JksKeypass: fJksKeypass, JksStorepass: fJksStorepass, @@ -172,7 +184,7 @@ func TestDeploy(t *testing.T) { return } - fstat, err := os.Stat(fOutputCertPath) + fstat, err := os.Stat(fOutputCertPath + ".jks") if err != nil { t.Errorf("err: %+v", err) return diff --git a/internal/pkg/core/deployer/providers/ssh/ssh.go b/internal/pkg/core/deployer/providers/ssh/ssh.go index e09c35b6..4fffce74 100644 --- a/internal/pkg/core/deployer/providers/ssh/ssh.go +++ b/internal/pkg/core/deployer/providers/ssh/ssh.go @@ -103,7 +103,7 @@ func (d *SshDeployer) Deploy(ctx context.Context, certPem string, privkeyPem str 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) + return nil, xerrors.Wrapf(err, "failed to execute pre-command: stdout: %s, stderr: %s", stdout, stderr) } d.logger.Logt("SSH pre-command executed", stdout) @@ -160,7 +160,7 @@ func (d *SshDeployer) Deploy(ctx context.Context, certPem string, privkeyPem str 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) + return nil, xerrors.Wrapf(err, "failed to execute post-command, stdout: %s, stderr: %s", stdout, stderr) } d.logger.Logt("SSH post-command executed", stdout) @@ -211,13 +211,13 @@ func execSshCommand(sshCli *ssh.Client, command string) (string, string, error) } defer session.Close() - var stdoutBuf bytes.Buffer - session.Stdout = &stdoutBuf - var stderrBuf bytes.Buffer - session.Stderr = &stderrBuf + stdoutBuf := bytes.NewBuffer(nil) + session.Stdout = stdoutBuf + stderrBuf := bytes.NewBuffer(nil) + session.Stderr = stderrBuf err = session.Run(command) if err != nil { - return "", "", err + return stdoutBuf.String(), stderrBuf.String(), xerrors.Wrap(err, "failed to execute ssh command") } return stdoutBuf.String(), stderrBuf.String(), nil diff --git a/internal/pkg/core/deployer/providers/ssh/ssh_test.go b/internal/pkg/core/deployer/providers/ssh/ssh_test.go index f83a539d..4221378e 100644 --- a/internal/pkg/core/deployer/providers/ssh/ssh_test.go +++ b/internal/pkg/core/deployer/providers/ssh/ssh_test.go @@ -69,6 +69,7 @@ func TestDeploy(t *testing.T) { SshPort: int32(fSshPort), SshUsername: fSshUsername, SshPassword: fSshPassword, + OutputFormat: provider.OUTPUT_FORMAT_PEM, OutputCertPath: fOutputCertPath, OutputKeyPath: fOutputKeyPath, }) From 8ecb71fb55c5727d8a99a0e6ff984a0423c00360 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 16 Jan 2025 20:27:30 +0800 Subject: [PATCH 2/8] refactor: clean code --- internal/certificate/service.go | 38 ++++---- internal/domain/access.go | 10 +- internal/domain/certificate.go | 1 + internal/domain/err.go | 33 ------- internal/domain/error.go | 30 ++++++ internal/notify/service.go | 6 +- internal/pkg/utils/types/types.go | 25 +++++ internal/repository/access.go | 17 +++- internal/repository/acme_account.go | 51 +++++++---- internal/repository/certificate.go | 91 ++++++++++++++++++- internal/repository/settings.go | 13 ++- internal/repository/workflow.go | 62 +++++++------ internal/repository/workflow_output.go | 48 ++-------- internal/rest/notify.go | 2 +- internal/rest/resp/resp.go | 9 +- internal/rest/statistics.go | 2 +- internal/rest/workflow.go | 2 +- internal/statistics/service.go | 6 +- .../workflow/node-processor/apply_node.go | 27 ++---- .../workflow/node-processor/condition_node.go | 6 +- .../workflow/node-processor/deploy_node.go | 12 ++- .../workflow/node-processor/notify_node.go | 17 ++-- internal/workflow/node-processor/processor.go | 27 ++++-- .../workflow/node-processor/start_node.go | 6 +- internal/workflow/service.go | 10 +- migrations/1737019549_updated_certificate.go | 54 +++++++++++ ui/src/api/notify.ts | 2 +- ui/src/api/statistics.ts | 4 +- ui/src/api/workflow.ts | 18 +--- ui/src/components/workflow/WorkflowRuns.tsx | 6 +- ui/src/domain/certificate.ts | 2 +- ui/src/pages/accesses/AccessList.tsx | 6 +- ui/src/pages/certificates/CertificateList.tsx | 6 +- ui/src/pages/dashboard/Dashboard.tsx | 10 +- ui/src/pages/workflows/WorkflowDetail.tsx | 4 +- ui/src/pages/workflows/WorkflowList.tsx | 6 +- ui/src/repository/certificate.ts | 9 +- ui/src/repository/workflow.ts | 14 ++- ui/types/global.d.ts | 6 ++ 39 files changed, 440 insertions(+), 258 deletions(-) delete mode 100644 internal/domain/err.go create mode 100644 internal/domain/error.go create mode 100644 internal/pkg/utils/types/types.go create mode 100644 migrations/1737019549_updated_certificate.go diff --git a/internal/certificate/service.go b/internal/certificate/service.go index e2bdb0f1..a4558e74 100644 --- a/internal/certificate/service.go +++ b/internal/certificate/service.go @@ -17,21 +17,21 @@ const ( defaultExpireMessage = "有 ${COUNT} 张证书即将过期,域名分别为 ${DOMAINS},请保持关注!" ) -type CertificateRepository interface { - ListExpireSoon(ctx context.Context) ([]domain.Certificate, error) +type certificateRepository interface { + ListExpireSoon(ctx context.Context) ([]*domain.Certificate, error) } -type certificateService struct { - repo CertificateRepository +type CertificateService struct { + repo certificateRepository } -func NewCertificateService(repo CertificateRepository) *certificateService { - return &certificateService{ +func NewCertificateService(repo certificateRepository) *CertificateService { + return &CertificateService{ repo: repo, } } -func (s *certificateService) InitSchedule(ctx context.Context) error { +func (s *CertificateService) InitSchedule(ctx context.Context) error { scheduler := app.GetScheduler() err := scheduler.Add("certificate", "0 0 * * *", func() { certs, err := s.repo.ListExpireSoon(context.Background()) @@ -58,13 +58,11 @@ func (s *certificateService) InitSchedule(ctx context.Context) error { return nil } -type certificateNotification struct { - Subject string `json:"subject"` - Message string `json:"message"` -} - -func buildExpireSoonNotification(records []domain.Certificate) *certificateNotification { - if len(records) == 0 { +func buildExpireSoonNotification(certificates []*domain.Certificate) *struct { + Subject string + Message string +} { + if len(certificates) == 0 { return nil } @@ -85,9 +83,9 @@ func buildExpireSoonNotification(records []domain.Certificate) *certificateNotif } // 替换变量 - count := len(records) + count := len(certificates) domains := make([]string, count) - for i, record := range records { + for i, record := range certificates { domains[i] = record.SubjectAltNames } countStr := strconv.Itoa(count) @@ -98,8 +96,8 @@ func buildExpireSoonNotification(records []domain.Certificate) *certificateNotif message = strings.ReplaceAll(message, "${DOMAINS}", domainStr) // 返回消息 - return &certificateNotification{ - Subject: subject, - Message: message, - } + return &struct { + Subject string + Message string + }{Subject: subject, Message: message} } diff --git a/internal/domain/access.go b/internal/domain/access.go index c9b1d2a9..c7cf08fd 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -7,11 +7,11 @@ import ( type Access struct { Meta - Name string `json:"name" db:"name"` - Provider string `json:"provider" db:"provider"` - Config string `json:"config" db:"config"` - Usage string `json:"usage" db:"usage"` - DeletedAt time.Time `json:"deleted" db:"deleted"` + Name string `json:"name" db:"name"` + Provider string `json:"provider" db:"provider"` + Config string `json:"config" db:"config"` + Usage string `json:"usage" db:"usage"` + DeletedAt *time.Time `json:"deleted" db:"deleted"` } func (a *Access) UnmarshalConfigToMap() (map[string]any, error) { diff --git a/internal/domain/certificate.go b/internal/domain/certificate.go index 2fe0c131..84179c3a 100644 --- a/internal/domain/certificate.go +++ b/internal/domain/certificate.go @@ -23,4 +23,5 @@ type Certificate struct { 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"` } diff --git a/internal/domain/err.go b/internal/domain/err.go deleted file mode 100644 index 15d43186..00000000 --- a/internal/domain/err.go +++ /dev/null @@ -1,33 +0,0 @@ -package domain - -var ( - ErrInvalidParams = NewXError(400, "invalid params") - ErrRecordNotFound = NewXError(404, "record not found") -) - -func IsRecordNotFound(err error) bool { - if e, ok := err.(*XError); ok { - return e.GetCode() == ErrRecordNotFound.GetCode() - } - return false -} - -type XError struct { - Code int `json:"code"` - Msg string `json:"msg"` -} - -func NewXError(code int, msg string) *XError { - return &XError{code, msg} -} - -func (e *XError) Error() string { - return e.Msg -} - -func (e *XError) GetCode() int { - if e.Code == 0 { - return 100 - } - return e.Code -} diff --git a/internal/domain/error.go b/internal/domain/error.go new file mode 100644 index 00000000..39dd64b9 --- /dev/null +++ b/internal/domain/error.go @@ -0,0 +1,30 @@ +package domain + +var ( + ErrInvalidParams = NewError(400, "invalid params") + ErrRecordNotFound = NewError(404, "record not found") +) + +type Error struct { + Code int `json:"code"` + Msg string `json:"msg"` +} + +func NewError(code int, msg string) *Error { + if code == 0 { + code = -1 + } + + return &Error{code, msg} +} + +func (e *Error) Error() string { + return e.Msg +} + +func IsRecordNotFoundError(err error) bool { + if e, ok := err.(*Error); ok { + return e.Code == ErrRecordNotFound.Code + } + return false +} diff --git a/internal/notify/service.go b/internal/notify/service.go index 8c99d824..0e414950 100644 --- a/internal/notify/service.go +++ b/internal/notify/service.go @@ -12,15 +12,15 @@ const ( notifyTestBody = "欢迎使用 Certimate ,这是一条测试通知。" ) -type SettingsRepository interface { +type settingsRepository interface { GetByName(ctx context.Context, name string) (*domain.Settings, error) } type NotifyService struct { - settingRepo SettingsRepository + settingRepo settingsRepository } -func NewNotifyService(settingRepo SettingsRepository) *NotifyService { +func NewNotifyService(settingRepo settingsRepository) *NotifyService { return &NotifyService{ settingRepo: settingRepo, } diff --git a/internal/pkg/utils/types/types.go b/internal/pkg/utils/types/types.go new file mode 100644 index 00000000..b88467b1 --- /dev/null +++ b/internal/pkg/utils/types/types.go @@ -0,0 +1,25 @@ +package types + +import "reflect" + +// 判断对象是否为 nil。 +// +// 入参: +// - value:待判断的对象。 +// +// 出参: +// - 如果对象值为 nil,则返回 true;否则返回 false。 +func IsNil(obj any) bool { + if obj == nil { + return true + } + + v := reflect.ValueOf(obj) + if v.Kind() == reflect.Ptr { + return v.IsNil() + } else if v.Kind() == reflect.Interface { + return v.Elem().IsNil() + } + + return false +} diff --git a/internal/repository/access.go b/internal/repository/access.go index 9646dcc9..9612c123 100644 --- a/internal/repository/access.go +++ b/internal/repository/access.go @@ -4,9 +4,12 @@ import ( "context" "database/sql" "errors" + "fmt" + "github.com/pocketbase/pocketbase/models" "github.com/usual2970/certimate/internal/app" "github.com/usual2970/certimate/internal/domain" + "github.com/usual2970/certimate/internal/pkg/utils/types" ) type AccessRepository struct{} @@ -15,7 +18,7 @@ func NewAccessRepository() *AccessRepository { return &AccessRepository{} } -func (a *AccessRepository) GetById(ctx context.Context, id string) (*domain.Access, error) { +func (r *AccessRepository) GetById(ctx context.Context, id string) (*domain.Access, error) { record, err := app.GetApp().Dao().FindRecordById("access", id) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -24,6 +27,18 @@ func (a *AccessRepository) GetById(ctx context.Context, id string) (*domain.Acce return nil, err } + if !types.IsNil(record.Get("deleted")) { + return nil, domain.ErrRecordNotFound + } + + return r.castRecordToModel(record) +} + +func (r *AccessRepository) castRecordToModel(record *models.Record) (*domain.Access, error) { + if record == nil { + return nil, fmt.Errorf("record is nil") + } + access := &domain.Access{ Meta: domain.Meta{ Id: record.GetId(), diff --git a/internal/repository/acme_account.go b/internal/repository/acme_account.go index 3289ced4..f5996fe9 100644 --- a/internal/repository/acme_account.go +++ b/internal/repository/acme_account.go @@ -22,7 +22,11 @@ var g singleflight.Group func (r *AcmeAccountRepository) GetByCAAndEmail(ca, email string) (*domain.AcmeAccount, error) { resp, err, _ := g.Do(fmt.Sprintf("acme_account_%s_%s", ca, email), func() (interface{}, error) { - resp, err := app.GetApp().Dao().FindFirstRecordByFilter("acme_accounts", "ca={:ca} && email={:email}", dbx.Params{"ca": ca, "email": email}) + resp, err := app.GetApp().Dao().FindFirstRecordByFilter( + "acme_accounts", + "ca={:ca} && email={:email}", + dbx.Params{"ca": ca, "email": email}, + ) if err != nil { return nil, err } @@ -33,30 +37,15 @@ func (r *AcmeAccountRepository) GetByCAAndEmail(ca, email string) (*domain.AcmeA } if resp == nil { - return nil, fmt.Errorf("acme account not found") + return nil, domain.ErrRecordNotFound } record, ok := resp.(*models.Record) if !ok { - return nil, fmt.Errorf("acme account not found") + return nil, domain.ErrRecordNotFound } - resource := ®istration.Resource{} - if err := record.UnmarshalJSONField("resource", resource); err != nil { - return nil, err - } - - return &domain.AcmeAccount{ - Meta: domain.Meta{ - Id: record.GetId(), - CreatedAt: record.GetCreated().Time(), - UpdatedAt: record.GetUpdated().Time(), - }, - CA: record.GetString("ca"), - Email: record.GetString("email"), - Key: record.GetString("key"), - Resource: resource, - }, nil + return r.castRecordToModel(record) } func (r *AcmeAccountRepository) Save(ca, email, key string, resource *registration.Resource) error { @@ -72,3 +61,27 @@ func (r *AcmeAccountRepository) Save(ca, email, key string, resource *registrati record.Set("resource", resource) return app.GetApp().Dao().Save(record) } + +func (r *AcmeAccountRepository) castRecordToModel(record *models.Record) (*domain.AcmeAccount, error) { + if record == nil { + return nil, fmt.Errorf("record is nil") + } + + resource := ®istration.Resource{} + if err := record.UnmarshalJSONField("resource", resource); err != nil { + return nil, err + } + + acmeAccount := &domain.AcmeAccount{ + Meta: domain.Meta{ + Id: record.GetId(), + CreatedAt: record.GetCreated().Time(), + UpdatedAt: record.GetUpdated().Time(), + }, + CA: record.GetString("ca"), + Email: record.GetString("email"), + Key: record.GetString("key"), + Resource: resource, + } + return acmeAccount, nil +} diff --git a/internal/repository/certificate.go b/internal/repository/certificate.go index 9bce7a9e..1ff31ac0 100644 --- a/internal/repository/certificate.go +++ b/internal/repository/certificate.go @@ -2,9 +2,15 @@ package repository import ( "context" + "database/sql" + "errors" + "fmt" + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/models" "github.com/usual2970/certimate/internal/app" "github.com/usual2970/certimate/internal/domain" + "github.com/usual2970/certimate/internal/pkg/utils/types" ) type CertificateRepository struct{} @@ -13,14 +19,89 @@ func NewCertificateRepository() *CertificateRepository { return &CertificateRepository{} } -func (c *CertificateRepository) ListExpireSoon(ctx context.Context) ([]domain.Certificate, error) { - certificates := []domain.Certificate{} - err := app.GetApp().Dao().DB(). - NewQuery("SELECT * FROM certificate WHERE expireAt > DATETIME('now') AND expireAt < DATETIME('now', '+20 days')"). - All(&certificates) +func (r *CertificateRepository) ListExpireSoon(ctx context.Context) ([]*domain.Certificate, error) { + records, err := app.GetApp().Dao().FindRecordsByFilter( + "certificate", + "expireAt>DATETIME('now') && expireAt validityDuration { a.AddOutput(ctx, a.node.Name, "已申请过证书,且证书在有效期内") diff --git a/internal/workflow/node-processor/condition_node.go b/internal/workflow/node-processor/condition_node.go index 742b3be9..cd3ab07f 100644 --- a/internal/workflow/node-processor/condition_node.go +++ b/internal/workflow/node-processor/condition_node.go @@ -8,13 +8,13 @@ import ( type conditionNode struct { node *domain.WorkflowNode - *Logger + *nodeLogger } func NewConditionNode(node *domain.WorkflowNode) *conditionNode { return &conditionNode{ - node: node, - Logger: NewLogger(node), + node: node, + nodeLogger: NewNodeLogger(node), } } diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go index 1623e84c..135e8cfd 100644 --- a/internal/workflow/node-processor/deploy_node.go +++ b/internal/workflow/node-processor/deploy_node.go @@ -12,15 +12,17 @@ import ( type deployNode struct { node *domain.WorkflowNode - outputRepo WorkflowOutputRepository - *Logger + certRepo certificateRepository + outputRepo workflowOutputRepository + *nodeLogger } func NewDeployNode(node *domain.WorkflowNode) *deployNode { return &deployNode{ node: node, - Logger: NewLogger(node), + nodeLogger: NewNodeLogger(node), outputRepo: repository.NewWorkflowOutputRepository(), + certRepo: repository.NewCertificateRepository(), } } @@ -28,7 +30,7 @@ func (d *deployNode) Run(ctx context.Context) error { d.AddOutput(ctx, d.node.Name, "开始执行") // 检查是否部署过(部署过则直接返回,和 v0.2 暂时保持一致) output, err := d.outputRepo.GetByNodeId(ctx, d.node.Id) - if err != nil && !domain.IsRecordNotFound(err) { + if err != nil && !domain.IsRecordNotFoundError(err) { d.AddOutput(ctx, d.node.Name, "查询部署记录失败", err.Error()) return err } @@ -42,7 +44,7 @@ func (d *deployNode) Run(ctx context.Context) error { return fmt.Errorf("证书来源配置错误: %s", certSource) } - cert, err := d.outputRepo.GetCertificateByNodeId(ctx, certSourceSlice[0]) + cert, err := d.certRepo.GetByWorkflowNodeId(ctx, certSourceSlice[0]) if err != nil { d.AddOutput(ctx, d.node.Name, "获取证书失败", err.Error()) return err diff --git a/internal/workflow/node-processor/notify_node.go b/internal/workflow/node-processor/notify_node.go index 42d071b0..ffb40566 100644 --- a/internal/workflow/node-processor/notify_node.go +++ b/internal/workflow/node-processor/notify_node.go @@ -8,20 +8,17 @@ import ( "github.com/usual2970/certimate/internal/repository" ) -type SettingRepository interface { - GetByName(ctx context.Context, name string) (*domain.Settings, error) -} type notifyNode struct { - node *domain.WorkflowNode - settingRepo SettingRepository - *Logger + node *domain.WorkflowNode + settingsRepo settingRepository + *nodeLogger } func NewNotifyNode(node *domain.WorkflowNode) *notifyNode { return ¬ifyNode{ - node: node, - Logger: NewLogger(node), - settingRepo: repository.NewSettingsRepository(), + node: node, + nodeLogger: NewNodeLogger(node), + settingsRepo: repository.NewSettingsRepository(), } } @@ -29,7 +26,7 @@ func (n *notifyNode) Run(ctx context.Context) error { n.AddOutput(ctx, n.node.Name, "开始执行") // 获取通知配置 - setting, err := n.settingRepo.GetByName(ctx, "notifyChannels") + setting, err := n.settingsRepo.GetByName(ctx, "notifyChannels") if err != nil { n.AddOutput(ctx, n.node.Name, "获取通知配置失败", err.Error()) return err diff --git a/internal/workflow/node-processor/processor.go b/internal/workflow/node-processor/processor.go index fca0ee0a..128022ac 100644 --- a/internal/workflow/node-processor/processor.go +++ b/internal/workflow/node-processor/processor.go @@ -8,18 +8,18 @@ import ( "github.com/usual2970/certimate/internal/domain" ) -type NodeProcessor interface { +type nodeProcessor interface { Run(ctx context.Context) error Log(ctx context.Context) *domain.WorkflowRunLog AddOutput(ctx context.Context, title, content string, err ...string) } -type Logger struct { +type nodeLogger struct { log *domain.WorkflowRunLog } -func NewLogger(node *domain.WorkflowNode) *Logger { - return &Logger{ +func NewNodeLogger(node *domain.WorkflowNode) *nodeLogger { + return &nodeLogger{ log: &domain.WorkflowRunLog{ NodeId: node.Id, NodeName: node.Name, @@ -28,11 +28,11 @@ func NewLogger(node *domain.WorkflowNode) *Logger { } } -func (l *Logger) Log(ctx context.Context) *domain.WorkflowRunLog { +func (l *nodeLogger) Log(ctx context.Context) *domain.WorkflowRunLog { return l.log } -func (l *Logger) AddOutput(ctx context.Context, title, content string, err ...string) { +func (l *nodeLogger) AddOutput(ctx context.Context, title, content string, err ...string) { output := domain.WorkflowRunLogOutput{ Time: time.Now().UTC().Format(time.RFC3339), Title: title, @@ -45,7 +45,7 @@ func (l *Logger) AddOutput(ctx context.Context, title, content string, err ...st l.log.Outputs = append(l.log.Outputs, output) } -func GetProcessor(node *domain.WorkflowNode) (NodeProcessor, error) { +func GetProcessor(node *domain.WorkflowNode) (nodeProcessor, error) { switch node.Type { case domain.WorkflowNodeTypeStart: return NewStartNode(node), nil @@ -60,3 +60,16 @@ func GetProcessor(node *domain.WorkflowNode) (NodeProcessor, error) { } return nil, errors.New("not implemented") } + +type certificateRepository interface { + GetByWorkflowNodeId(ctx context.Context, workflowNodeId string) (*domain.Certificate, error) +} + +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 +} + +type settingRepository interface { + GetByName(ctx context.Context, name string) (*domain.Settings, error) +} diff --git a/internal/workflow/node-processor/start_node.go b/internal/workflow/node-processor/start_node.go index 682b77d6..6dc641ab 100644 --- a/internal/workflow/node-processor/start_node.go +++ b/internal/workflow/node-processor/start_node.go @@ -8,13 +8,13 @@ import ( type startNode struct { node *domain.WorkflowNode - *Logger + *nodeLogger } func NewStartNode(node *domain.WorkflowNode) *startNode { return &startNode{ - node: node, - Logger: NewLogger(node), + node: node, + nodeLogger: NewNodeLogger(node), } } diff --git a/internal/workflow/service.go b/internal/workflow/service.go index 83afa94b..93b4c51a 100644 --- a/internal/workflow/service.go +++ b/internal/workflow/service.go @@ -19,21 +19,21 @@ type workflowRunData struct { Options *domain.WorkflowRunReq } -type WorkflowRepository interface { +type workflowRepository interface { + ListEnabledAuto(ctx context.Context) ([]*domain.Workflow, error) GetById(ctx context.Context, id string) (*domain.Workflow, error) - SaveRun(ctx context.Context, run *domain.WorkflowRun) error Save(ctx context.Context, workflow *domain.Workflow) error - ListEnabledAuto(ctx context.Context) ([]domain.Workflow, error) + SaveRun(ctx context.Context, run *domain.WorkflowRun) error } type WorkflowService struct { ch chan *workflowRunData - repo WorkflowRepository + repo workflowRepository wg sync.WaitGroup cancel context.CancelFunc } -func NewWorkflowService(repo WorkflowRepository) *WorkflowService { +func NewWorkflowService(repo workflowRepository) *WorkflowService { rs := &WorkflowService{ repo: repo, ch: make(chan *workflowRunData, 1), diff --git a/migrations/1737019549_updated_certificate.go b/migrations/1737019549_updated_certificate.go new file mode 100644 index 00000000..cef2a645 --- /dev/null +++ b/migrations/1737019549_updated_certificate.go @@ -0,0 +1,54 @@ +package migrations + +import ( + "encoding/json" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + m "github.com/pocketbase/pocketbase/migrations" + "github.com/pocketbase/pocketbase/models/schema" +) + +func init() { + m.Register(func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("4szxr9x43tpj6np") + if err != nil { + return err + } + + // add + new_deleted := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "klyf4nlq", + "name": "deleted", + "type": "date", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": "", + "max": "" + } + }`), new_deleted); err != nil { + return err + } + collection.Schema.AddField(new_deleted) + + return dao.SaveCollection(collection) + }, func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("4szxr9x43tpj6np") + if err != nil { + return err + } + + // remove + collection.Schema.RemoveField("klyf4nlq") + + return dao.SaveCollection(collection) + }) +} diff --git a/ui/src/api/notify.ts b/ui/src/api/notify.ts index 6eb078ab..e0167175 100644 --- a/ui/src/api/notify.ts +++ b/ui/src/api/notify.ts @@ -5,7 +5,7 @@ import { getPocketBase } from "@/repository/pocketbase"; export const notifyTest = async (channel: string) => { const pb = getPocketBase(); - const resp = await pb.send("/api/notify/test", { + const resp = await pb.send("/api/notify/test", { method: "POST", headers: { "Content-Type": "application/json", diff --git a/ui/src/api/statistics.ts b/ui/src/api/statistics.ts index 6c1d422c..f05dda0c 100644 --- a/ui/src/api/statistics.ts +++ b/ui/src/api/statistics.ts @@ -6,7 +6,7 @@ import { getPocketBase } from "@/repository/pocketbase"; export const get = async () => { const pb = getPocketBase(); - const resp = await pb.send("/api/statistics/get", { + const resp = await pb.send>("/api/statistics/get", { method: "GET", }); @@ -14,5 +14,5 @@ export const get = async () => { throw new ClientResponseError({ status: resp.code, response: resp, data: {} }); } - return resp.data as Statistics; + return resp; }; diff --git a/ui/src/api/workflow.ts b/ui/src/api/workflow.ts index c40fcce8..afead94e 100644 --- a/ui/src/api/workflow.ts +++ b/ui/src/api/workflow.ts @@ -1,12 +1,12 @@ -import { ClientResponseError, type RecordSubscription } from "pocketbase"; +import { ClientResponseError } from "pocketbase"; -import { WORKFLOW_TRIGGERS, type WorkflowModel } from "@/domain/workflow"; +import { WORKFLOW_TRIGGERS } from "@/domain/workflow"; import { getPocketBase } from "@/repository/pocketbase"; export const run = async (id: string) => { const pb = getPocketBase(); - const resp = await pb.send("/api/workflow/run", { + const resp = await pb.send("/api/workflow/run", { method: "POST", headers: { "Content-Type": "application/json", @@ -23,15 +23,3 @@ export const run = async (id: string) => { return resp; }; - -export const subscribe = async (id: string, cb: (e: RecordSubscription) => void) => { - const pb = getPocketBase(); - - return pb.collection("workflow").subscribe(id, cb); -}; - -export const unsubscribe = async (id: string) => { - const pb = getPocketBase(); - - return pb.collection("workflow").unsubscribe(id); -}; diff --git a/ui/src/components/workflow/WorkflowRuns.tsx b/ui/src/components/workflow/WorkflowRuns.tsx index 6686d906..df5a93dc 100644 --- a/ui/src/components/workflow/WorkflowRuns.tsx +++ b/ui/src/components/workflow/WorkflowRuns.tsx @@ -139,9 +139,9 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => { }, { refreshDeps: [workflowId, page, pageSize], - onSuccess: (data) => { - setTableData(data.items); - setTableTotal(data.totalItems); + onSuccess: (res) => { + setTableData(res.items); + setTableTotal(res.totalItems); }, onError: (err) => { if (err instanceof ClientResponseError && err.isAbort) { diff --git a/ui/src/domain/certificate.ts b/ui/src/domain/certificate.ts index 33d6571e..00c0d7d5 100644 --- a/ui/src/domain/certificate.ts +++ b/ui/src/domain/certificate.ts @@ -8,7 +8,7 @@ export interface CertificateModel extends BaseModel { effectAt: ISO8601String; expireAt: ISO8601String; workflowId: string; - expand: { + expand?: { workflowId?: WorkflowModel; // TODO: ugly, maybe to use an alias? }; } diff --git a/ui/src/pages/accesses/AccessList.tsx b/ui/src/pages/accesses/AccessList.tsx index 6cd1c128..ba7d59d8 100644 --- a/ui/src/pages/accesses/AccessList.tsx +++ b/ui/src/pages/accesses/AccessList.tsx @@ -142,9 +142,9 @@ const AccessList = () => { }, { refreshDeps: [accesses, page, pageSize], - onSuccess: (data) => { - setTableData(data.items); - setTableTotal(data.totalItems); + onSuccess: (res) => { + setTableData(res.items); + setTableTotal(res.totalItems); }, } ); diff --git a/ui/src/pages/certificates/CertificateList.tsx b/ui/src/pages/certificates/CertificateList.tsx index 0a845faf..7ee33b7c 100644 --- a/ui/src/pages/certificates/CertificateList.tsx +++ b/ui/src/pages/certificates/CertificateList.tsx @@ -204,9 +204,9 @@ const CertificateList = () => { }, { refreshDeps: [filters, page, pageSize], - onSuccess: (data) => { - setTableData(data.items); - setTableTotal(data.totalItems); + onSuccess: (res) => { + setTableData(res.items); + setTableTotal(res.totalItems); }, onError: (err) => { if (err instanceof ClientResponseError && err.isAbort) { diff --git a/ui/src/pages/dashboard/Dashboard.tsx b/ui/src/pages/dashboard/Dashboard.tsx index 82e0acea..b9870850 100644 --- a/ui/src/pages/dashboard/Dashboard.tsx +++ b/ui/src/pages/dashboard/Dashboard.tsx @@ -164,9 +164,9 @@ const Dashboard = () => { }, { refreshDeps: [page, pageSize], - onSuccess: (data) => { - setTableData(data.items); - setTableTotal(data.totalItems > 3 ? 3 : data.totalItems); + onSuccess: (res) => { + setTableData(res.items); + setTableTotal(res.totalItems > 3 ? 3 : res.totalItems); }, onError: (err) => { if (err instanceof ClientResponseError && err.isAbort) { @@ -193,8 +193,8 @@ const Dashboard = () => { return getStatistics(); }, { - onSuccess: (data) => { - setStatistics(data); + onSuccess: (res) => { + setStatistics(res.data); }, onError: (err) => { if (err instanceof ClientResponseError && err.isAbort) { diff --git a/ui/src/pages/workflows/WorkflowDetail.tsx b/ui/src/pages/workflows/WorkflowDetail.tsx index 543bf244..826703b3 100644 --- a/ui/src/pages/workflows/WorkflowDetail.tsx +++ b/ui/src/pages/workflows/WorkflowDetail.tsx @@ -16,7 +16,7 @@ import { createSchemaFieldRule } from "antd-zod"; import { isEqual } from "radash"; import { z } from "zod"; -import { run as runWorkflow, subscribe as subscribeWorkflow, unsubscribe as unsubscribeWorkflow } from "@/api/workflow"; +import { run as runWorkflow } from "@/api/workflow"; import ModalForm from "@/components/ModalForm"; import Show from "@/components/Show"; import WorkflowElements from "@/components/workflow/WorkflowElements"; @@ -24,7 +24,7 @@ import WorkflowRuns from "@/components/workflow/WorkflowRuns"; import { isAllNodesValidated } from "@/domain/workflow"; import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun"; import { useAntdForm, useZustandShallowSelector } from "@/hooks"; -import { remove as removeWorkflow } from "@/repository/workflow"; +import { remove as removeWorkflow, subscribe as subscribeWorkflow, unsubscribe as unsubscribeWorkflow } from "@/repository/workflow"; import { useWorkflowStore } from "@/stores/workflow"; import { getErrMsg } from "@/utils/error"; diff --git a/ui/src/pages/workflows/WorkflowList.tsx b/ui/src/pages/workflows/WorkflowList.tsx index 967d8410..fa3f9154 100644 --- a/ui/src/pages/workflows/WorkflowList.tsx +++ b/ui/src/pages/workflows/WorkflowList.tsx @@ -250,9 +250,9 @@ const WorkflowList = () => { }, { refreshDeps: [filters, page, pageSize], - onSuccess: (data) => { - setTableData(data.items); - setTableTotal(data.totalItems); + onSuccess: (res) => { + setTableData(res.items); + setTableTotal(res.totalItems); }, onError: (err) => { if (err instanceof ClientResponseError && err.isAbort) { diff --git a/ui/src/repository/certificate.ts b/ui/src/repository/certificate.ts index 0589c116..0654dc28 100644 --- a/ui/src/repository/certificate.ts +++ b/ui/src/repository/certificate.ts @@ -19,17 +19,18 @@ export const list = async (request: ListCertificateRequest) => { const perPage = request.perPage || 10; const options: RecordListOptions = { - sort: "-created", expand: "workflowId", + filter: "deleted=null", + sort: "-created", requestKey: null, }; if (request.state === "expireSoon") { - options.filter = pb.filter("expireAt<{:expiredAt}", { - expiredAt: dayjs().add(15, "d").toDate(), + options.filter = pb.filter("expireAt<{:expiredAt} && deleted=null", { + expiredAt: dayjs().add(20, "d").toDate(), }); } else if (request.state === "expired") { - options.filter = pb.filter("expireAt<={:expiredAt}", { + options.filter = pb.filter("expireAt<={:expiredAt} && deleted=null", { expiredAt: new Date(), }); } diff --git a/ui/src/repository/workflow.ts b/ui/src/repository/workflow.ts index dc16b2ea..4410aa52 100644 --- a/ui/src/repository/workflow.ts +++ b/ui/src/repository/workflow.ts @@ -1,4 +1,4 @@ -import { type RecordListOptions } from "pocketbase"; +import { type RecordListOptions, type RecordSubscription } from "pocketbase"; import { type WorkflowModel } from "@/domain/workflow"; import { getPocketBase } from "./pocketbase"; @@ -48,3 +48,15 @@ export const save = async (record: MaybeModelRecord) => { export const remove = async (record: MaybeModelRecordWithId) => { return await getPocketBase().collection(COLLECTION_NAME).delete(record.id); }; + +export const subscribe = async (id: string, cb: (e: RecordSubscription) => void) => { + const pb = getPocketBase(); + + return pb.collection("workflow").subscribe(id, cb); +}; + +export const unsubscribe = async (id: string) => { + const pb = getPocketBase(); + + return pb.collection("workflow").unsubscribe(id); +}; diff --git a/ui/types/global.d.ts b/ui/types/global.d.ts index 9c4f16f7..3334b33f 100644 --- a/ui/types/global.d.ts +++ b/ui/types/global.d.ts @@ -12,6 +12,12 @@ declare global { declare type MaybeModelRecord = T | Omit; declare type MaybeModelRecordWithId = T | Pick; + + declare interface BaseResponse { + code: number; + msg: string; + data: T; + } } export {}; From 831f0ee5d9221338133da59d65ef9ecb1b7eec82 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 16 Jan 2025 21:50:16 +0800 Subject: [PATCH 3/8] feat(ui): improve responsive ui --- ui/src/i18n/locales/zh/nls.dashboard.json | 5 +- ui/src/pages/dashboard/Dashboard.tsx | 125 +++++++++------------- 2 files changed, 50 insertions(+), 80 deletions(-) diff --git a/ui/src/i18n/locales/zh/nls.dashboard.json b/ui/src/i18n/locales/zh/nls.dashboard.json index 471ebfce..badd7cb8 100644 --- a/ui/src/i18n/locales/zh/nls.dashboard.json +++ b/ui/src/i18n/locales/zh/nls.dashboard.json @@ -13,7 +13,6 @@ "dashboard.quick_actions": "快捷操作", "dashboard.quick_actions.create_workflow": "新建工作流", "dashboard.quick_actions.change_login_password": "修改登录密码", - "dashboard.quick_actions.notification_settings": "消息推送设置", - "dashboard.quick_actions.certificate_authority_configuration": "证书颁发机构配置" + "dashboard.quick_actions.cofigure_notification": "消息推送设置", + "dashboard.quick_actions.configure_ca": "证书颁发机构配置" } - diff --git a/ui/src/pages/dashboard/Dashboard.tsx b/ui/src/pages/dashboard/Dashboard.tsx index b9870850..a98d08f3 100644 --- a/ui/src/pages/dashboard/Dashboard.tsx +++ b/ui/src/pages/dashboard/Dashboard.tsx @@ -29,31 +29,54 @@ import { ClientResponseError } from "pocketbase"; import { get as getStatistics } from "@/api/statistics"; import WorkflowRunDetailDrawer from "@/components/workflow/WorkflowRunDetailDrawer"; import { type Statistics } from "@/domain/statistics"; -import { WORKFLOW_TRIGGERS } from "@/domain/workflow"; import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun"; import { list as listWorkflowRuns } from "@/repository/workflowRun"; import { getErrMsg } from "@/utils/error"; -const { useBreakpoint } = Grid; - const Dashboard = () => { const navigate = useNavigate(); - const screens = useBreakpoint(); - const { t } = useTranslation(); const { token: themeToken } = theme.useToken(); + const breakpoints = Grid.useBreakpoint(); const [notificationApi, NotificationContextHolder] = notification.useNotification(); + const statisticsGridSpans = { + xs: { flex: "50%" }, + md: { flex: "50%" }, + lg: { flex: "33.3333%" }, + xl: { flex: "33.3333%" }, + xxl: { flex: "20%" }, + }; + const [statistics, setStatistics] = useState(); + const { loading: statisticsLoading } = useRequest( + () => { + return getStatistics(); + }, + { + onSuccess: (res) => { + setStatistics(res.data); + }, + onError: (err) => { + if (err instanceof ClientResponseError && err.isAbort) { + return; + } + + console.error(err); + notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); + }, + } + ); + const tableColumns: TableProps["columns"] = [ { key: "$index", align: "center", fixed: "left", width: 50, - render: (_, __, index) => (page - 1) * pageSize + index + 1, + render: (_, __, index) => index + 1, }, { key: "name", @@ -98,20 +121,6 @@ const Dashboard = () => { return <>; }, }, - { - key: "trigger", - title: t("workflow_run.props.trigger"), - ellipsis: true, - render: (_, record) => { - if (record.trigger === WORKFLOW_TRIGGERS.AUTO) { - return t("workflow_run.props.trigger.auto"); - } else if (record.trigger === WORKFLOW_TRIGGERS.MANUAL) { - return t("workflow_run.props.trigger.manual"); - } - - return <>; - }, - }, { key: "startedAt", title: t("workflow_run.props.started_at"), @@ -139,7 +148,6 @@ const Dashboard = () => { { key: "$action", align: "end", - fixed: "right", width: 120, render: (_, record) => ( @@ -149,52 +157,17 @@ const Dashboard = () => { }, ]; const [tableData, setTableData] = useState([]); - const [_tableTotal, setTableTotal] = useState(0); - - const [page, _setPage] = useState(1); - const [pageSize, _setPageSize] = useState(3); - - const { loading: loadingWorkflowRun } = useRequest( + const { loading: tableLoading } = useRequest( () => { return listWorkflowRuns({ - page: page, - perPage: pageSize, + page: 1, + perPage: 5, expand: true, }); }, { - refreshDeps: [page, pageSize], onSuccess: (res) => { setTableData(res.items); - setTableTotal(res.totalItems > 3 ? 3 : res.totalItems); - }, - onError: (err) => { - if (err instanceof ClientResponseError && err.isAbort) { - return; - } - - console.error(err); - notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); - }, - } - ); - - const statisticsGridSpans = { - xs: { flex: "50%" }, - md: { flex: "50%" }, - lg: { flex: "33.3333%" }, - xl: { flex: "33.3333%" }, - xxl: { flex: "20%" }, - }; - const [statistics, setStatistics] = useState(); - - const { loading } = useRequest( - () => { - return getStatistics(); - }, - { - onSuccess: (res) => { - setStatistics(res.data); }, onError: (err) => { if (err instanceof ClientResponseError && err.isAbort) { @@ -218,7 +191,7 @@ const Dashboard = () => { } label={t("dashboard.statistics.all_certificates")} - loading={loading} + loading={statisticsLoading} value={statistics?.certificateTotal ?? "-"} suffix={t("dashboard.statistics.unit")} onClick={() => navigate("/certificates")} @@ -228,7 +201,7 @@ const Dashboard = () => { } label={t("dashboard.statistics.expire_soon_certificates")} - loading={loading} + loading={statisticsLoading} value={statistics?.certificateExpireSoon ?? "-"} suffix={t("dashboard.statistics.unit")} onClick={() => navigate("/certificates?state=expireSoon")} @@ -238,7 +211,7 @@ const Dashboard = () => { } label={t("dashboard.statistics.expired_certificates")} - loading={loading} + loading={statisticsLoading} value={statistics?.certificateExpired ?? "-"} suffix={t("dashboard.statistics.unit")} onClick={() => navigate("/certificates?state=expired")} @@ -248,7 +221,7 @@ const Dashboard = () => { } label={t("dashboard.statistics.all_workflows")} - loading={loading} + loading={statisticsLoading} value={statistics?.workflowTotal ?? "-"} suffix={t("dashboard.statistics.unit")} onClick={() => navigate("/workflows")} @@ -258,7 +231,7 @@ const Dashboard = () => { } label={t("dashboard.statistics.enabled_workflows")} - loading={loading} + loading={statisticsLoading} value={statistics?.workflowEnabled ?? "-"} suffix={t("dashboard.statistics.unit")} onClick={() => navigate("/workflows?state=enabled")} @@ -268,34 +241,32 @@ const Dashboard = () => { - - -
{t("dashboard.quick_actions")}
-
- - - - -
+
- -
{t("dashboard.latest_workflow_run")}
+ - className="mt-5" columns={tableColumns} dataSource={tableData} - loading={loadingWorkflowRun} + loading={tableLoading} locale={{ emptyText: , }} + pagination={false} rowKey={(record: WorkflowRunModel) => record.id} scroll={{ x: "max(100%, 960px)" }} /> From 3a2baba746a6341daeeeaee618da2537274d5c63 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 16 Jan 2025 21:53:51 +0800 Subject: [PATCH 4/8] feat: support removing certificates --- internal/certificate/service.go | 6 ++-- internal/repository/access.go | 3 +- internal/repository/certificate.go | 3 +- ui/src/domain/settings.ts | 2 +- ui/src/i18n/locales/en/nls.certificate.json | 1 + ui/src/i18n/locales/en/nls.dashboard.json | 4 +-- ui/src/i18n/locales/zh/nls.access.json | 2 +- ui/src/i18n/locales/zh/nls.certificate.json | 1 + ui/src/i18n/locales/zh/nls.workflow.json | 2 +- .../i18n/locales/zh/nls.workflow.nodes.json | 2 +- ui/src/pages/accesses/AccessList.tsx | 3 +- ui/src/pages/certificates/CertificateList.tsx | 36 +++++++++++++------ ui/src/pages/dashboard/Dashboard.tsx | 4 +-- ui/src/pages/workflows/WorkflowDetail.tsx | 2 +- ui/src/pages/workflows/WorkflowList.tsx | 5 +-- ui/src/repository/access.ts | 1 + ui/src/repository/certificate.ts | 1 + 17 files changed, 48 insertions(+), 30 deletions(-) diff --git a/internal/certificate/service.go b/internal/certificate/service.go index a4558e74..af46c202 100644 --- a/internal/certificate/service.go +++ b/internal/certificate/service.go @@ -13,7 +13,7 @@ import ( ) const ( - defaultExpireSubject = "您有 ${COUNT} 张证书即将过期" + defaultExpireSubject = "有 ${COUNT} 张证书即将过期" defaultExpireMessage = "有 ${COUNT} 张证书即将过期,域名分别为 ${DOMAINS},请保持关注!" ) @@ -36,7 +36,7 @@ func (s *CertificateService) InitSchedule(ctx context.Context) error { err := scheduler.Add("certificate", "0 0 * * *", func() { certs, err := s.repo.ListExpireSoon(context.Background()) if err != nil { - app.GetLogger().Error("failed to get expire soon certificate", "err", err) + app.GetLogger().Error("failed to get certificates which expire soon", "err", err) return } @@ -46,7 +46,7 @@ func (s *CertificateService) InitSchedule(ctx context.Context) error { } if err := notify.SendToAllChannels(notification.Subject, notification.Message); err != nil { - app.GetLogger().Error("failed to send expire soon certificate", "err", err) + app.GetLogger().Error("failed to send notification", "err", err) } }) if err != nil { diff --git a/internal/repository/access.go b/internal/repository/access.go index 9612c123..9244a112 100644 --- a/internal/repository/access.go +++ b/internal/repository/access.go @@ -9,7 +9,6 @@ import ( "github.com/pocketbase/pocketbase/models" "github.com/usual2970/certimate/internal/app" "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/pkg/utils/types" ) type AccessRepository struct{} @@ -27,7 +26,7 @@ func (r *AccessRepository) GetById(ctx context.Context, id string) (*domain.Acce return nil, err } - if !types.IsNil(record.Get("deleted")) { + if !record.GetDateTime("deleted").Time().IsZero() { return nil, domain.ErrRecordNotFound } diff --git a/internal/repository/certificate.go b/internal/repository/certificate.go index 1ff31ac0..0499eb48 100644 --- a/internal/repository/certificate.go +++ b/internal/repository/certificate.go @@ -10,7 +10,6 @@ import ( "github.com/pocketbase/pocketbase/models" "github.com/usual2970/certimate/internal/app" "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/pkg/utils/types" ) type CertificateRepository struct{} @@ -52,7 +51,7 @@ func (r *CertificateRepository) GetById(ctx context.Context, id string) (*domain return nil, err } - if !types.IsNil(record.Get("deleted")) { + if !record.GetDateTime("deleted").Time().IsZero() { return nil, domain.ErrRecordNotFound } diff --git a/ui/src/domain/settings.ts b/ui/src/domain/settings.ts index 9df3dc05..977d80fc 100644 --- a/ui/src/domain/settings.ts +++ b/ui/src/domain/settings.ts @@ -29,7 +29,7 @@ export type NotifyTemplate = { }; export const defaultNotifyTemplate: NotifyTemplate = { - subject: "您有 ${COUNT} 张证书即将过期", + subject: "有 ${COUNT} 张证书即将过期", message: "有 ${COUNT} 张证书即将过期,域名分别为 ${DOMAINS},请保持关注!", }; // #endregion diff --git a/ui/src/i18n/locales/en/nls.certificate.json b/ui/src/i18n/locales/en/nls.certificate.json index dd9aa4c2..deb6508d 100644 --- a/ui/src/i18n/locales/en/nls.certificate.json +++ b/ui/src/i18n/locales/en/nls.certificate.json @@ -5,6 +5,7 @@ "certificate.action.view": "View certificate", "certificate.action.delete": "Delete certificate", + "certificate.action.delete.confirm": "Are you sure to delete this certificate?", "certificate.action.download": "Download certificate", "certificate.props.subject_alt_names": "Name", diff --git a/ui/src/i18n/locales/en/nls.dashboard.json b/ui/src/i18n/locales/en/nls.dashboard.json index e179f20f..8ae9d94d 100644 --- a/ui/src/i18n/locales/en/nls.dashboard.json +++ b/ui/src/i18n/locales/en/nls.dashboard.json @@ -13,6 +13,6 @@ "dashboard.quick_actions": "Quick actions", "dashboard.quick_actions.create_workflow": "Create workflow", "dashboard.quick_actions.change_login_password": "Change login password", - "dashboard.quick_actions.notification_settings": "Notification settings", - "dashboard.quick_actions.certificate_authority_configuration": "Certificate authority configuration" + "dashboard.quick_actions.cofigure_notification": "Configure notificaion", + "dashboard.quick_actions.configure_ca": "Configure certificate authority" } diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index dc8d8c93..35a10eda 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -20,7 +20,7 @@ "access.form.name.placeholder": "请输入授权名称", "access.form.provider.label": "提供商", "access.form.provider.placeholder": "请选择提供商", - "access.form.provider.tooltip": "提供商分为两种类型:
【DNS 提供商】您的 DNS 托管方,通常等同于域名注册商,用于在申请证书时管理您的域名解析记录。
【主机提供商】您的服务器或云服务的托管方,用于部署签发的证书。

该字段保存后不可修改。", + "access.form.provider.tooltip": "提供商分为两种类型:
【DNS 提供商】你的 DNS 托管方,通常等同于域名注册商,用于在申请证书时管理您的域名解析记录。
【主机提供商】你的服务器或云服务的托管方,用于部署签发的证书。

该字段保存后不可修改。", "access.form.acmehttpreq_endpoint.label": "服务端点", "access.form.acmehttpreq_endpoint.placeholder": "请输入服务端点", "access.form.acmehttpreq_endpoint.tooltip": "这是什么?请参阅 https://go-acme.github.io/lego/dns/httpreq/", diff --git a/ui/src/i18n/locales/zh/nls.certificate.json b/ui/src/i18n/locales/zh/nls.certificate.json index bf14655b..f4a86d95 100644 --- a/ui/src/i18n/locales/zh/nls.certificate.json +++ b/ui/src/i18n/locales/zh/nls.certificate.json @@ -5,6 +5,7 @@ "certificate.action.view": "查看证书", "certificate.action.delete": "删除证书", + "certificate.action.delete.confirm": "确定要删除此证书吗?", "certificate.action.download": "下载证书", "certificate.props.subject_alt_names": "名称", diff --git a/ui/src/i18n/locales/zh/nls.workflow.json b/ui/src/i18n/locales/zh/nls.workflow.json index 46058991..31c726f9 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.json +++ b/ui/src/i18n/locales/zh/nls.workflow.json @@ -49,7 +49,7 @@ "workflow.detail.orchestration.action.release.confirm": "确定要发布更改吗?", "workflow.detail.orchestration.action.release.failed.uncompleted": "流程编排未完成,请检查是否有节点未配置", "workflow.detail.orchestration.action.run": "执行", - "workflow.detail.orchestration.action.run.confirm": "你有尚未发布的更改。你确定要以最近一次发布的版本继续执行吗?", + "workflow.detail.orchestration.action.run.confirm": "你有尚未发布的更改。确定要以最近一次发布的版本继续执行吗?", "workflow.detail.orchestration.action.run.prompt": "执行中……请稍后查看执行历史", "workflow.detail.runs.tab": "执行历史" } diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index b6291e06..9ea8c7dc 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -7,7 +7,7 @@ "workflow_node.action.rename_branch": "重命名", "workflow_node.action.remove_branch": "删除分支", - "workflow_node.unsaved_changes.confirm": "你有尚未保存的更改。你确定要关闭面板吗?", + "workflow_node.unsaved_changes.confirm": "你有尚未保存的更改。确定要关闭面板吗?", "workflow_node.start.label": "开始", "workflow_node.start.form.trigger.label": "触发方式", diff --git a/ui/src/pages/accesses/AccessList.tsx b/ui/src/pages/accesses/AccessList.tsx index ba7d59d8..6fa03647 100644 --- a/ui/src/pages/accesses/AccessList.tsx +++ b/ui/src/pages/accesses/AccessList.tsx @@ -130,7 +130,7 @@ const AccessList = () => { }); }, []); - const { loading } = useRequest( + const { loading, run: refreshTableData } = useRequest( () => { const startIndex = (page - 1) * pageSize; const endIndex = startIndex + pageSize; @@ -157,6 +157,7 @@ const AccessList = () => { // TODO: 有关联数据的不允许被删除 try { await deleteAccess(data); + refreshTableData(); } catch (err) { console.error(err); notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); diff --git a/ui/src/pages/certificates/CertificateList.tsx b/ui/src/pages/certificates/CertificateList.tsx index 7ee33b7c..a78c9326 100644 --- a/ui/src/pages/certificates/CertificateList.tsx +++ b/ui/src/pages/certificates/CertificateList.tsx @@ -4,13 +4,13 @@ import { useNavigate, useSearchParams } from "react-router-dom"; import { DeleteOutlined as DeleteOutlinedIcon, SelectOutlined as SelectOutlinedIcon } from "@ant-design/icons"; import { PageHeader } from "@ant-design/pro-components"; import { useRequest } from "ahooks"; -import { Button, Divider, Empty, Menu, type MenuProps, Radio, Space, Table, type TableProps, Tooltip, Typography, notification, theme } from "antd"; +import { Button, Divider, Empty, Menu, type MenuProps, Modal, Radio, Space, Table, type TableProps, Tooltip, Typography, notification, theme } from "antd"; import dayjs from "dayjs"; import { ClientResponseError } from "pocketbase"; import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer"; import { CERTIFICATE_SOURCES, type CertificateModel } from "@/domain/certificate"; -import { type ListCertificateRequest, list as listCertificate } from "@/repository/certificate"; +import { type ListCertificateRequest, list as listCertificate, remove as removeCertificate } from "@/repository/certificate"; import { getErrMsg } from "@/utils/error"; const CertificateList = () => { @@ -21,6 +21,7 @@ const CertificateList = () => { const { token: themeToken } = theme.useToken(); + const [modalApi, ModalContextHolder] = Modal.useModal(); const [notificationApi, NotificationContextHolder] = notification.useNotification(); const tableColumns: TableProps["columns"] = [ @@ -169,14 +170,7 @@ const CertificateList = () => { /> -
diff --git a/ui/src/pages/workflows/WorkflowDetail.tsx b/ui/src/pages/workflows/WorkflowDetail.tsx index 826703b3..f2f46398 100644 --- a/ui/src/pages/workflows/WorkflowDetail.tsx +++ b/ui/src/pages/workflows/WorkflowDetail.tsx @@ -109,7 +109,7 @@ const WorkflowDetail = () => { content: t("workflow.action.delete.confirm"), onOk: async () => { try { - const resp: boolean = await removeWorkflow(workflow); + const resp = await removeWorkflow(workflow); if (resp) { navigate("/workflows", { replace: true }); } diff --git a/ui/src/pages/workflows/WorkflowList.tsx b/ui/src/pages/workflows/WorkflowList.tsx index fa3f9154..30975917 100644 --- a/ui/src/pages/workflows/WorkflowList.tsx +++ b/ui/src/pages/workflows/WorkflowList.tsx @@ -240,7 +240,7 @@ const WorkflowList = () => { const [page, setPage] = useState(() => parseInt(+searchParams.get("page")! + "") || 1); const [pageSize, setPageSize] = useState(() => parseInt(+searchParams.get("perPage")! + "") || 10); - const { loading } = useRequest( + const { loading, run: refreshTableData } = useRequest( () => { return listWorkflow({ page: page, @@ -302,9 +302,10 @@ const WorkflowList = () => { content: t("workflow.action.delete.confirm"), onOk: async () => { try { - const resp: boolean = await removeWorkflow(workflow); + const resp = await removeWorkflow(workflow); if (resp) { setTableData((prev) => prev.filter((item) => item.id !== workflow.id)); + refreshTableData(); } } catch (err) { console.error(err); diff --git a/ui/src/repository/access.ts b/ui/src/repository/access.ts index a29b33d8..02d9827b 100644 --- a/ui/src/repository/access.ts +++ b/ui/src/repository/access.ts @@ -30,4 +30,5 @@ export const remove = async (record: MaybeModelRecordWithId) => { if ("provider" in record && record.provider === "pdns") record.provider = "powerdns"; await getPocketBase().collection(COLLECTION_NAME).update(record.id!, record); + return true; }; diff --git a/ui/src/repository/certificate.ts b/ui/src/repository/certificate.ts index 0654dc28..64ae9edb 100644 --- a/ui/src/repository/certificate.ts +++ b/ui/src/repository/certificate.ts @@ -42,4 +42,5 @@ export const remove = async (record: MaybeModelRecordWithId) = record = { ...record, deleted: dayjs.utc().format("YYYY-MM-DD HH:mm:ss") }; await getPocketBase().collection(COLLECTION_NAME).update(record.id!, record); + return true; }; From d1dbbae101f4ea7202fafc3a81236aeb71a2d02f Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 16 Jan 2025 22:07:01 +0800 Subject: [PATCH 5/8] feat(ui): show errmsg if table loaded error --- ui/src/components/workflow/WorkflowRuns.tsx | 6 ++++-- ui/src/pages/accesses/AccessList.tsx | 4 ++-- ui/src/pages/certificates/CertificateList.tsx | 14 ++++++++++---- ui/src/pages/dashboard/Dashboard.tsx | 4 ++++ ui/src/pages/workflows/WorkflowList.tsx | 13 ++++++++++--- 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/ui/src/components/workflow/WorkflowRuns.tsx b/ui/src/components/workflow/WorkflowRuns.tsx index df5a93dc..f716b8c4 100644 --- a/ui/src/components/workflow/WorkflowRuns.tsx +++ b/ui/src/components/workflow/WorkflowRuns.tsx @@ -129,7 +129,7 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => { const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); - const { loading } = useRequest( + const { loading, loadedError } = useRequest( () => { return listWorkflowRuns({ workflowId: workflowId, @@ -150,6 +150,8 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => { console.error(err); notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); + + throw err; }, } ); @@ -164,7 +166,7 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => { dataSource={tableData} loading={loading} locale={{ - emptyText: , + emptyText: , }} pagination={{ current: page, diff --git a/ui/src/pages/accesses/AccessList.tsx b/ui/src/pages/accesses/AccessList.tsx index 6fa03647..bbc7abe6 100644 --- a/ui/src/pages/accesses/AccessList.tsx +++ b/ui/src/pages/accesses/AccessList.tsx @@ -130,7 +130,7 @@ const AccessList = () => { }); }, []); - const { loading, run: refreshTableData } = useRequest( + const { loading, run: refreshData } = useRequest( () => { const startIndex = (page - 1) * pageSize; const endIndex = startIndex + pageSize; @@ -157,7 +157,7 @@ const AccessList = () => { // TODO: 有关联数据的不允许被删除 try { await deleteAccess(data); - refreshTableData(); + refreshData(); } catch (err) { console.error(err); notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); diff --git a/ui/src/pages/certificates/CertificateList.tsx b/ui/src/pages/certificates/CertificateList.tsx index a78c9326..b2b861e0 100644 --- a/ui/src/pages/certificates/CertificateList.tsx +++ b/ui/src/pages/certificates/CertificateList.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate, useSearchParams } from "react-router-dom"; -import { DeleteOutlined as DeleteOutlinedIcon, SelectOutlined as SelectOutlinedIcon } from "@ant-design/icons"; +import { DeleteOutlined as DeleteOutlinedIcon, SelectOutlined as SelectOutlinedIcon, WarningOutlined as WarningOutlinedIcon } from "@ant-design/icons"; import { PageHeader } from "@ant-design/pro-components"; import { useRequest } from "ahooks"; import { Button, Divider, Empty, Menu, type MenuProps, Modal, Radio, Space, Table, type TableProps, Tooltip, Typography, notification, theme } from "antd"; @@ -188,7 +188,11 @@ const CertificateList = () => { const [page, setPage] = useState(() => parseInt(+searchParams.get("page")! + "") || 1); const [pageSize, setPageSize] = useState(() => parseInt(+searchParams.get("perPage")! + "") || 10); - const { loading, run: refreshTableData } = useRequest( + const { + loading, + error: loadedError, + run: refreshData, + } = useRequest( () => { return listCertificate({ page: page, @@ -209,6 +213,8 @@ const CertificateList = () => { console.error(err); notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); + + throw err; }, } ); @@ -222,7 +228,7 @@ const CertificateList = () => { const resp = await removeCertificate(certificate); if (resp) { setTableData((prev) => prev.filter((item) => item.id !== certificate.id)); - refreshTableData(); + refreshData(); } } catch (err) { console.error(err); @@ -244,7 +250,7 @@ const CertificateList = () => { dataSource={tableData} loading={loading} locale={{ - emptyText: , + emptyText: , }} pagination={{ current: page, diff --git a/ui/src/pages/dashboard/Dashboard.tsx b/ui/src/pages/dashboard/Dashboard.tsx index 531bae58..be6323dd 100644 --- a/ui/src/pages/dashboard/Dashboard.tsx +++ b/ui/src/pages/dashboard/Dashboard.tsx @@ -66,6 +66,8 @@ const Dashboard = () => { console.error(err); notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); + + throw err; }, } ); @@ -176,6 +178,8 @@ const Dashboard = () => { console.error(err); notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); + + throw err; }, } ); diff --git a/ui/src/pages/workflows/WorkflowList.tsx b/ui/src/pages/workflows/WorkflowList.tsx index 30975917..ba1f7bb2 100644 --- a/ui/src/pages/workflows/WorkflowList.tsx +++ b/ui/src/pages/workflows/WorkflowList.tsx @@ -20,6 +20,7 @@ import { type MenuProps, Modal, Radio, + Result, Space, Switch, Table, @@ -240,7 +241,11 @@ const WorkflowList = () => { const [page, setPage] = useState(() => parseInt(+searchParams.get("page")! + "") || 1); const [pageSize, setPageSize] = useState(() => parseInt(+searchParams.get("perPage")! + "") || 10); - const { loading, run: refreshTableData } = useRequest( + const { + loading, + error: loadedError, + run: refreshData, + } = useRequest( () => { return listWorkflow({ page: page, @@ -261,6 +266,8 @@ const WorkflowList = () => { console.error(err); notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); + + throw err; }, } ); @@ -305,7 +312,7 @@ const WorkflowList = () => { const resp = await removeWorkflow(workflow); if (resp) { setTableData((prev) => prev.filter((item) => item.id !== workflow.id)); - refreshTableData(); + refreshData(); } } catch (err) { console.error(err); @@ -342,7 +349,7 @@ const WorkflowList = () => { dataSource={tableData} loading={loading} locale={{ - emptyText: , + emptyText: , }} pagination={{ current: page, From 087fd81879fc4c90ab9c26a91e9c0cfd28a2dab7 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 16 Jan 2025 22:23:00 +0800 Subject: [PATCH 6/8] feat: support configuring pb-data-dir on app launch --- internal/workflow/event.go | 4 +--- main.go | 25 ++++++++++++------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/internal/workflow/event.go b/internal/workflow/event.go index 2f209baa..aaad9822 100644 --- a/internal/workflow/event.go +++ b/internal/workflow/event.go @@ -14,7 +14,7 @@ import ( const tableName = "workflow" -func RegisterEvents() error { +func Register() { app := app.GetApp() app.OnRecordAfterCreateRequest(tableName).Add(func(e *core.RecordCreateEvent) error { @@ -28,8 +28,6 @@ func RegisterEvents() error { app.OnRecordAfterDeleteRequest(tableName).Add(func(e *core.RecordDeleteEvent) error { return delete(e.HttpContext.Request().Context(), e.Record) }) - - return nil } func update(ctx context.Context, record *models.Record) error { diff --git a/main.go b/main.go index bb34606f..2db632d6 100644 --- a/main.go +++ b/main.go @@ -25,27 +25,24 @@ import ( func main() { app := app.GetApp() - isGoRun := strings.HasPrefix(os.Args[0], os.TempDir()) - - // 获取启动命令中的http参数 - var httpFlag string - flag.StringVar(&httpFlag, "http", "127.0.0.1:8090", "HTTP server address") - // "serve"影响解析 - _ = flag.CommandLine.Parse(os.Args[2:]) + var flagHttp string + var flagDir string + flag.StringVar(&flagHttp, "http", "127.0.0.1:8090", "HTTP server address") + flag.StringVar(&flagDir, "dir", "/pb_data/database", "Pocketbase data directory") + _ = flag.CommandLine.Parse(os.Args[2:]) // skip the first two arguments: "main.go serve" migratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{ // enable auto creation of migration files when making collection changes in the Admin UI // (the isGoRun check is to enable it only during development) - Automigrate: isGoRun, + Automigrate: strings.HasPrefix(os.Args[0], os.TempDir()), }) - workflow.RegisterEvents() - app.OnBeforeServe().Add(func(e *core.ServeEvent) error { - routes.Register(e.Router) - scheduler.Register() + workflow.Register() + + routes.Register(e.Router) e.Router.GET( "/*", echo.StaticDirectoryHandler(ui.DistDirFS, false), @@ -57,11 +54,13 @@ func main() { app.OnTerminate().Add(func(e *core.TerminateEvent) error { routes.Unregister() + log.Println("Exit!") + return nil }) - log.Printf("Visit the website: http://%s", httpFlag) + log.Printf("Visit the website: http://%s", flagHttp) if err := app.Start(); err != nil { log.Fatal(err) From a20b82b9cf8f082d865f3e0ebfd832907484f9b7 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 16 Jan 2025 23:02:08 +0800 Subject: [PATCH 7/8] feat: re-run workflow nodes when critical configurations changed --- go.mod | 2 +- go.sum | 4 +- .../workflow/node-processor/apply_node.go | 89 ++++++++++++------- .../workflow/node-processor/deploy_node.go | 68 ++++++++------ .../workflow/node-processor/notify_node.go | 8 +- .../workflow/node-processor/start_node.go | 8 +- 6 files changed, 107 insertions(+), 72 deletions(-) diff --git a/go.mod b/go.mod index 89e2287e..152d66c6 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/volcengine/volc-sdk-golang v1.0.189 github.com/volcengine/volcengine-go-sdk v1.0.177 golang.org/x/crypto v0.32.0 - golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 + golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 k8s.io/api v0.32.0 k8s.io/apimachinery v0.32.0 k8s.io/client-go v0.32.0 diff --git a/go.sum b/go.sum index eda86588..57ff9916 100644 --- a/go.sum +++ b/go.sum @@ -957,8 +957,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= -golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index 30e28aad..4e20ebcb 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -5,6 +5,8 @@ import ( "strings" "time" + "golang.org/x/exp/maps" + "github.com/usual2970/certimate/internal/applicant" "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/pkg/utils/certs" @@ -29,35 +31,29 @@ func NewApplyNode(node *domain.WorkflowNode) *applyNode { // 申请节点根据申请类型执行不同的操作 func (a *applyNode) Run(ctx context.Context) error { - const validityDuration = time.Hour * 24 * 10 - a.AddOutput(ctx, a.node.Name, "开始执行") - // 查询是否申请过,已申请过则直接返回 - // TODO: 先保持和 v0.2 一致,后续增加是否强制申请的参数 - output, err := a.outputRepo.GetByNodeId(ctx, a.node.Id) + + // 查询上次执行结果 + lastOutput, err := a.outputRepo.GetByNodeId(ctx, a.node.Id) if err != nil && !domain.IsRecordNotFoundError(err) { a.AddOutput(ctx, a.node.Name, "查询申请记录失败", err.Error()) return err } - if output != nil && output.Succeeded { - lastCertificate, _ := a.certRepo.GetByWorkflowNodeId(ctx, a.node.Id) - if lastCertificate != nil { - if time.Until(lastCertificate.ExpireAt) > validityDuration { - a.AddOutput(ctx, a.node.Name, "已申请过证书,且证书在有效期内") - return nil - } - } + // 检测是否可以跳过本次执行 + if skippable, skipReason := a.checkCanSkip(ctx, lastOutput); skippable { + a.AddOutput(ctx, a.node.Name, skipReason) + return nil } - // 获取Applicant + // 初始化申请器 applicant, err := applicant.NewWithApplyNode(a.node) if err != nil { a.AddOutput(ctx, a.node.Name, "获取申请对象失败", err.Error()) return err } - // 申请 + // 申请证书 applyResult, err := applicant.Apply() if err != nil { a.AddOutput(ctx, a.node.Name, "申请失败", err.Error()) @@ -65,27 +61,12 @@ func (a *applyNode) Run(ctx context.Context) error { } a.AddOutput(ctx, a.node.Name, "申请成功") - // 记录申请结果 - // 保持一个节点只有一个输出 - outputId := "" - if output != nil { - outputId = output.Id - } - output = &domain.WorkflowOutput{ - Meta: domain.Meta{Id: outputId}, - WorkflowId: GetWorkflowId(ctx), - NodeId: a.node.Id, - Node: a.node, - Succeeded: true, - Outputs: a.node.Outputs, - } - + // 解析证书并生成实体 certX509, err := certs.ParseCertificateFromPEM(applyResult.CertificateFullChain) if err != nil { a.AddOutput(ctx, a.node.Name, "解析证书失败", err.Error()) return err } - certificate := &domain.Certificate{ Source: domain.CertificateSourceTypeWorkflow, SubjectAltNames: strings.Join(certX509.DNSNames, ";"), @@ -100,7 +81,19 @@ func (a *applyNode) Run(ctx context.Context) error { WorkflowNodeId: a.node.Id, } - if err := a.outputRepo.Save(ctx, output, certificate, func(id string) error { + // 保存执行结果 + // TODO: 先保持一个节点始终只有一个输出,后续增加版本控制 + currentOutput := &domain.WorkflowOutput{ + WorkflowId: GetWorkflowId(ctx), + NodeId: a.node.Id, + Node: a.node, + Succeeded: true, + Outputs: a.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 } @@ -110,8 +103,38 @@ func (a *applyNode) Run(ctx context.Context) error { a.AddOutput(ctx, a.node.Name, "保存申请记录失败", err.Error()) return err } - a.AddOutput(ctx, a.node.Name, "保存申请记录成功") return nil } + +func (a *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (skip bool, reason string) { + const validityDuration = time.Hour * 24 * 10 + + // TODO: 可控制是否强制申请 + if lastOutput != nil && lastOutput.Succeeded { + // 比较和上次申请时的关键配置(即影响证书签发的)参数是否一致 + if lastOutput.Node.GetConfigString("domains") != a.node.GetConfigString("domains") { + return false, "配置项变化:域名" + } + if lastOutput.Node.GetConfigString("contactEmail") != a.node.GetConfigString("contactEmail") { + return false, "配置项变化:联系邮箱" + } + if lastOutput.Node.GetConfigString("provider") != a.node.GetConfigString("provider") { + return false, "配置项变化:DNS 提供商授权" + } + if !maps.Equal(lastOutput.Node.GetConfigMap("providerConfig"), a.node.GetConfigMap("providerConfig")) { + return false, "配置项变化:DNS 提供商参数" + } + if lastOutput.Node.GetConfigString("keyAlgorithm") != a.node.GetConfigString("keyAlgorithm") { + return false, "配置项变化:数字签名算法" + } + + lastCertificate, _ := a.certRepo.GetByWorkflowNodeId(ctx, a.node.Id) + if lastCertificate != nil && time.Until(lastCertificate.ExpireAt) > validityDuration { + return true, "已申请过证书,且证书尚未临近过期" + } + } + + return false, "无历史申请记录" +} diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go index 135e8cfd..6b4fb512 100644 --- a/internal/workflow/node-processor/deploy_node.go +++ b/internal/workflow/node-processor/deploy_node.go @@ -8,6 +8,7 @@ import ( "github.com/usual2970/certimate/internal/deployer" "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/repository" + "golang.org/x/exp/maps" ) type deployNode struct { @@ -28,77 +29,88 @@ func NewDeployNode(node *domain.WorkflowNode) *deployNode { func (d *deployNode) Run(ctx context.Context) error { d.AddOutput(ctx, d.node.Name, "开始执行") - // 检查是否部署过(部署过则直接返回,和 v0.2 暂时保持一致) - output, err := d.outputRepo.GetByNodeId(ctx, d.node.Id) + + // 查询上次执行结果 + lastOutput, err := d.outputRepo.GetByNodeId(ctx, d.node.Id) if err != nil && !domain.IsRecordNotFoundError(err) { d.AddOutput(ctx, d.node.Name, "查询部署记录失败", err.Error()) return err } - // 获取部署对象 - // 获取证书 - certSource := d.node.GetConfigString("certificate") + // 获取前序节点输出证书 + certSource := d.node.GetConfigString("certificate") certSourceSlice := strings.Split(certSource, "#") if len(certSourceSlice) != 2 { d.AddOutput(ctx, d.node.Name, "证书来源配置错误", certSource) return fmt.Errorf("证书来源配置错误: %s", certSource) } - - cert, err := d.certRepo.GetByWorkflowNodeId(ctx, certSourceSlice[0]) + certificate, err := d.certRepo.GetByWorkflowNodeId(ctx, certSourceSlice[0]) if err != nil { d.AddOutput(ctx, d.node.Name, "获取证书失败", err.Error()) return err } - // 未部署过,开始部署 - // 部署过但是证书更新了,重新部署 - // 部署过且证书未更新,直接返回 - - if d.deployed(output) && cert.CreatedAt.Before(output.UpdatedAt) { - d.AddOutput(ctx, d.node.Name, "已部署过且证书未更新") + // 检测是否可以跳过本次执行 + if skippable, skipReason := d.checkCanSkip(ctx, lastOutput); skippable { + if certificate.CreatedAt.Before(lastOutput.UpdatedAt) { + d.AddOutput(ctx, d.node.Name, "已部署过且证书未更新") + } else { + d.AddOutput(ctx, d.node.Name, skipReason) + } return nil } + // 初始化部署器 deploy, err := deployer.NewWithDeployNode(d.node, struct { Certificate string PrivateKey string - }{Certificate: cert.Certificate, PrivateKey: cert.PrivateKey}) + }{Certificate: certificate.Certificate, PrivateKey: certificate.PrivateKey}) if err != nil { d.AddOutput(ctx, d.node.Name, "获取部署对象失败", err.Error()) return err } - // 部署 + // 部署证书 if err := deploy.Deploy(ctx); err != nil { d.AddOutput(ctx, d.node.Name, "部署失败", err.Error()) return err } - d.AddOutput(ctx, d.node.Name, "部署成功") - // 记录部署结果 - outputId := "" - if output != nil { - outputId = output.Id - } - output = &domain.WorkflowOutput{ - Meta: domain.Meta{Id: outputId}, + // 保存执行结果 + // TODO: 先保持一个节点始终只有一个输出,后续增加版本控制 + currentOutput := &domain.WorkflowOutput{ + Meta: domain.Meta{}, WorkflowId: GetWorkflowId(ctx), NodeId: d.node.Id, Node: d.node, Succeeded: true, } - - if err := d.outputRepo.Save(ctx, output, nil, nil); err != nil { + 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()) return err } - d.AddOutput(ctx, d.node.Name, "保存部署记录成功") return nil } -func (d *deployNode) deployed(output *domain.WorkflowOutput) bool { - return output != nil && output.Succeeded +func (d *deployNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (skip bool, reason string) { + // TODO: 可控制是否强制部署 + if lastOutput != nil && lastOutput.Succeeded { + // 比较和上次部署时的关键配置(即影响证书部署的)参数是否一致 + if lastOutput.Node.GetConfigString("provider") != d.node.GetConfigString("provider") { + return false, "配置项变化:主机提供商授权" + } + if !maps.Equal(lastOutput.Node.GetConfigMap("providerConfig"), d.node.GetConfigMap("providerConfig")) { + return false, "配置项变化:主机提供商参数" + } + + return true, "已部署过证书" + } + + return false, "无历史部署记录" } diff --git a/internal/workflow/node-processor/notify_node.go b/internal/workflow/node-processor/notify_node.go index ffb40566..cbc7fa47 100644 --- a/internal/workflow/node-processor/notify_node.go +++ b/internal/workflow/node-processor/notify_node.go @@ -26,18 +26,20 @@ func (n *notifyNode) Run(ctx context.Context) error { n.AddOutput(ctx, n.node.Name, "开始执行") // 获取通知配置 - setting, err := n.settingsRepo.GetByName(ctx, "notifyChannels") + settings, err := n.settingsRepo.GetByName(ctx, "notifyChannels") if err != nil { n.AddOutput(ctx, n.node.Name, "获取通知配置失败", err.Error()) return err } - channelConfig, err := setting.GetNotifyChannelConfig(n.node.GetConfigString("channel")) + // 获取通知渠道 + channelConfig, err := settings.GetNotifyChannelConfig(n.node.GetConfigString("channel")) if err != nil { n.AddOutput(ctx, n.node.Name, "获取通知渠道配置失败", err.Error()) return err } + // 发送通知 if err := notify.SendToChannel(n.node.GetConfigString("subject"), n.node.GetConfigString("message"), n.node.GetConfigString("channel"), @@ -46,7 +48,7 @@ func (n *notifyNode) Run(ctx context.Context) error { n.AddOutput(ctx, n.node.Name, "发送通知失败", err.Error()) return err } - n.AddOutput(ctx, n.node.Name, "发送通知成功") + return nil } diff --git a/internal/workflow/node-processor/start_node.go b/internal/workflow/node-processor/start_node.go index 6dc641ab..81d93de6 100644 --- a/internal/workflow/node-processor/start_node.go +++ b/internal/workflow/node-processor/start_node.go @@ -18,11 +18,9 @@ func NewStartNode(node *domain.WorkflowNode) *startNode { } } -// 开始节点没有任何操作 func (s *startNode) Run(ctx context.Context) error { - s.AddOutput(ctx, - s.node.Name, - "完成", - ) + // 开始节点没有任何操作 + s.AddOutput(ctx, s.node.Name, "完成") + return nil } From dab6ad917c62124f64cfcd9255952a4c9eff632b Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 16 Jan 2025 23:42:53 +0800 Subject: [PATCH 8/8] refactor: remove unused code --- ui/src/components/workflow/WorkflowRuns.tsx | 2 +- ui/src/pages/certificates/CertificateList.tsx | 2 +- ui/src/pages/workflows/WorkflowList.tsx | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ui/src/components/workflow/WorkflowRuns.tsx b/ui/src/components/workflow/WorkflowRuns.tsx index f716b8c4..ef9fd5ea 100644 --- a/ui/src/components/workflow/WorkflowRuns.tsx +++ b/ui/src/components/workflow/WorkflowRuns.tsx @@ -129,7 +129,7 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => { const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); - const { loading, loadedError } = useRequest( + const { loading, error: loadedError } = useRequest( () => { return listWorkflowRuns({ workflowId: workflowId, diff --git a/ui/src/pages/certificates/CertificateList.tsx b/ui/src/pages/certificates/CertificateList.tsx index b2b861e0..3c7aa98f 100644 --- a/ui/src/pages/certificates/CertificateList.tsx +++ b/ui/src/pages/certificates/CertificateList.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate, useSearchParams } from "react-router-dom"; -import { DeleteOutlined as DeleteOutlinedIcon, SelectOutlined as SelectOutlinedIcon, WarningOutlined as WarningOutlinedIcon } from "@ant-design/icons"; +import { DeleteOutlined as DeleteOutlinedIcon, SelectOutlined as SelectOutlinedIcon } from "@ant-design/icons"; import { PageHeader } from "@ant-design/pro-components"; import { useRequest } from "ahooks"; import { Button, Divider, Empty, Menu, type MenuProps, Modal, Radio, Space, Table, type TableProps, Tooltip, Typography, notification, theme } from "antd"; diff --git a/ui/src/pages/workflows/WorkflowList.tsx b/ui/src/pages/workflows/WorkflowList.tsx index ba1f7bb2..f997b98d 100644 --- a/ui/src/pages/workflows/WorkflowList.tsx +++ b/ui/src/pages/workflows/WorkflowList.tsx @@ -20,7 +20,6 @@ import { type MenuProps, Modal, Radio, - Result, Space, Switch, Table,