From d11fc1c07e16676deb3eae5903d352286ce57c36 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 5 Feb 2025 21:04:28 +0800 Subject: [PATCH 01/23] refactor: reimpl qiniu sdk --- .../lego-providers/gname/internal/lego.go | 2 - internal/pkg/vendors/qiniu-sdk/auth.go | 29 +++++ internal/pkg/vendors/qiniu-sdk/client.go | 114 +++--------------- internal/pkg/vendors/qiniu-sdk/models.go | 2 +- 4 files changed, 48 insertions(+), 99 deletions(-) create mode 100644 internal/pkg/vendors/qiniu-sdk/auth.go diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/gname/internal/lego.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/gname/internal/lego.go index 979a803b..03dc633f 100644 --- a/internal/pkg/core/applicant/acme-dns-01/lego-providers/gname/internal/lego.go +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/gname/internal/lego.go @@ -173,8 +173,6 @@ func (d *DNSProvider) addOrUpdateDNSRecord(domain, subDomain, value string) erro _, err := d.client.ModifyDomainResolution(request) return err } - - return nil } func (d *DNSProvider) removeDNSRecord(domain, subDomain, value string) error { diff --git a/internal/pkg/vendors/qiniu-sdk/auth.go b/internal/pkg/vendors/qiniu-sdk/auth.go new file mode 100644 index 00000000..d27fbe03 --- /dev/null +++ b/internal/pkg/vendors/qiniu-sdk/auth.go @@ -0,0 +1,29 @@ +package qiniusdk + +import ( + "net/http" + + "github.com/qiniu/go-sdk/v7/auth" +) + +type transport struct { + http.RoundTripper + mac *auth.Credentials +} + +func newTransport(mac *auth.Credentials, tr http.RoundTripper) *transport { + if tr == nil { + tr = http.DefaultTransport + } + return &transport{tr, mac} +} + +func (t *transport) RoundTrip(req *http.Request) (resp *http.Response, err error) { + token, err := t.mac.SignRequestV2(req) + if err != nil { + return + } + + req.Header.Set("Authorization", "Qiniu "+token) + return t.RoundTripper.RoundTrip(req) +} diff --git a/internal/pkg/vendors/qiniu-sdk/client.go b/internal/pkg/vendors/qiniu-sdk/client.go index 1f036afd..4f342564 100644 --- a/internal/pkg/vendors/qiniu-sdk/client.go +++ b/internal/pkg/vendors/qiniu-sdk/client.go @@ -1,44 +1,36 @@ package qiniusdk import ( - "bytes" - "encoding/json" + "context" "fmt" - "io" "net/http" "strings" "github.com/qiniu/go-sdk/v7/auth" + "github.com/qiniu/go-sdk/v7/client" ) const qiniuHost = "https://api.qiniu.com" type Client struct { - mac *auth.Credentials + client *client.Client } func NewClient(mac *auth.Credentials) *Client { if mac == nil { mac = auth.Default() } - return &Client{mac: mac} + + client := client.DefaultClient + client.Transport = newTransport(mac, nil) + return &Client{client: &client} } func (c *Client) GetDomainInfo(domain string) (*GetDomainInfoResponse, error) { - respBytes, err := c.sendReq(http.MethodGet, fmt.Sprintf("domain/%s", domain), nil) - if err != nil { + resp := new(GetDomainInfoResponse) + if err := c.client.Call(context.Background(), resp, http.MethodGet, c.urlf("domain/%s", domain), nil); err != nil { return nil, err } - - resp := &GetDomainInfoResponse{} - err = json.Unmarshal(respBytes, resp) - if err != nil { - return nil, err - } - if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 { - return nil, fmt.Errorf("code: %d, error: %s", *resp.Code, *resp.Error) - } - return resp, nil } @@ -50,26 +42,10 @@ func (c *Client) ModifyDomainHttpsConf(domain, certId string, forceHttps, http2E Http2Enable: http2Enable, }, } - - reqBytes, err := json.Marshal(req) - if err != nil { + resp := new(ModifyDomainHttpsConfResponse) + if err := c.client.CallWithJson(context.Background(), resp, http.MethodPut, c.urlf("domain/%s/httpsconf", domain), nil, req); err != nil { return nil, err } - - respBytes, err := c.sendReq(http.MethodPut, fmt.Sprintf("domain/%s/httpsconf", domain), bytes.NewReader(reqBytes)) - if err != nil { - return nil, err - } - - resp := &ModifyDomainHttpsConfResponse{} - err = json.Unmarshal(respBytes, resp) - if err != nil { - return nil, err - } - if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 { - return nil, fmt.Errorf("code: %d, error: %s", *resp.Code, *resp.Error) - } - return resp, nil } @@ -81,26 +57,10 @@ func (c *Client) EnableDomainHttps(domain, certId string, forceHttps, http2Enabl Http2Enable: http2Enable, }, } - - reqBytes, err := json.Marshal(req) - if err != nil { + resp := new(EnableDomainHttpsResponse) + if err := c.client.CallWithJson(context.Background(), resp, http.MethodPut, c.urlf("domain/%s/sslize", domain), nil, req); err != nil { return nil, err } - - respBytes, err := c.sendReq(http.MethodPut, fmt.Sprintf("domain/%s/sslize", domain), bytes.NewReader(reqBytes)) - if err != nil { - return nil, err - } - - resp := &EnableDomainHttpsResponse{} - err = json.Unmarshal(respBytes, resp) - if err != nil { - return nil, err - } - if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 { - return nil, fmt.Errorf("code: %d, error: %s", *resp.Code, *resp.Error) - } - return resp, nil } @@ -111,53 +71,15 @@ func (c *Client) UploadSslCert(name, commonName, certificate, privateKey string) Certificate: certificate, PrivateKey: privateKey, } - - reqBytes, err := json.Marshal(req) - if err != nil { + resp := new(UploadSslCertResponse) + if err := c.client.CallWithJson(context.Background(), resp, http.MethodPost, c.urlf("sslcert"), nil, req); err != nil { return nil, err } - - respBytes, err := c.sendReq(http.MethodPost, "sslcert", bytes.NewReader(reqBytes)) - if err != nil { - return nil, err - } - - resp := &UploadSslCertResponse{} - err = json.Unmarshal(respBytes, resp) - if err != nil { - return nil, err - } - if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 { - return nil, fmt.Errorf("qiniu api error, code: %d, error: %s", *resp.Code, *resp.Error) - } - return resp, nil } -func (c *Client) sendReq(method string, path string, body io.Reader) ([]byte, error) { +func (c *Client) urlf(pathf string, pathargs ...any) string { + path := fmt.Sprintf(pathf, pathargs...) path = strings.TrimPrefix(path, "/") - - req, err := http.NewRequest(method, fmt.Sprintf("%s/%s", qiniuHost, path), body) - if err != nil { - return nil, err - } - req.Header.Add("Content-Type", "application/json") - - if err := c.mac.AddToken(auth.TokenQBox, req); err != nil { - return nil, err - } - - client := http.Client{} - resp, err := client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - r, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - return r, nil + return qiniuHost + "/" + path } diff --git a/internal/pkg/vendors/qiniu-sdk/models.go b/internal/pkg/vendors/qiniu-sdk/models.go index 3c68ee6b..dceca028 100644 --- a/internal/pkg/vendors/qiniu-sdk/models.go +++ b/internal/pkg/vendors/qiniu-sdk/models.go @@ -13,7 +13,7 @@ type UploadSslCertRequest struct { } type UploadSslCertResponse struct { - *BaseResponse + BaseResponse CertID string `json:"certID"` } From 98f4f1cc99bf3efac8e44d706143b1fa16e621ae Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 5 Feb 2025 22:42:46 +0800 Subject: [PATCH 02/23] fix: conflict pocketbase superuser initializations --- main.go | 3 ++- migrations/1737141502_superusers_initial.go | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/main.go b/main.go index 1928c46b..2c3d84c2 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,8 @@ import ( "github.com/usual2970/certimate/internal/scheduler" "github.com/usual2970/certimate/internal/workflow" "github.com/usual2970/certimate/ui" - //_ "github.com/usual2970/certimate/migrations" + + _ "github.com/usual2970/certimate/migrations" ) func main() { diff --git a/migrations/1737141502_superusers_initial.go b/migrations/1737141502_superusers_initial.go index a9dd9522..4440ed76 100644 --- a/migrations/1737141502_superusers_initial.go +++ b/migrations/1737141502_superusers_initial.go @@ -12,16 +12,16 @@ func init() { return err } - record := core.NewRecord(superusers) - record.Set("email", "admin@certimate.fun") - record.Set("password", "1234567890") - return app.Save(record) - }, func(app core.App) error { record, _ := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "admin@certimate.fun") if record == nil { - return nil + record := core.NewRecord(superusers) + record.Set("email", "admin@certimate.fun") + record.Set("password", "1234567890") + return app.Save(record) } - return app.Delete(record) + return nil + }, func(app core.App) error { + return nil }) } From bc29cce645c056b1042df02de6a6ddb40e1cb533 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 5 Feb 2025 22:56:55 +0800 Subject: [PATCH 03/23] chore(deps): upgrade gomod dependencies --- go.mod | 68 ++++++++++++------------- go.sum | 156 ++++++++++++++++++++++++++------------------------------- 2 files changed, 102 insertions(+), 122 deletions(-) diff --git a/go.mod b/go.mod index 6cd082bf..cc7c0ff1 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0 github.com/pkg/sftp v1.13.7 github.com/pocketbase/dbx v1.11.0 - github.com/pocketbase/pocketbase v0.24.4 + github.com/pocketbase/pocketbase v0.25.0 github.com/povsister/scp v0.0.0-20240802064259-28781e87b246 github.com/qiniu/go-sdk/v7 v7.25.2 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1084 @@ -42,7 +42,7 @@ require ( github.com/volcengine/volc-sdk-golang v1.0.193 github.com/volcengine/volcengine-go-sdk v1.0.178 golang.org/x/crypto v0.32.0 - golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 + golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c k8s.io/api v0.32.1 k8s.io/apimachinery v0.32.1 k8s.io/client-go v0.32.1 @@ -108,7 +108,6 @@ require ( ) require ( - github.com/AlecAivazis/survey/v2 v2.3.7 // indirect github.com/BurntSushi/toml v1.4.0 // indirect github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect github.com/alibabacloud-go/dcdn-20180115/v3 v3.5.0 @@ -120,25 +119,25 @@ require ( github.com/aliyun/alibaba-cloud-sdk-go v1.63.83 // indirect github.com/aliyun/credentials-go v1.4.3 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/aws/aws-sdk-go-v2 v1.33.0 - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect - github.com/aws/aws-sdk-go-v2/config v1.29.1 - github.com/aws/aws-sdk-go-v2/credentials v1.17.54 - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 // indirect - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.52 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.28 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.2 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.9 // indirect - github.com/aws/aws-sdk-go-v2/service/s3 v1.73.2 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.24.11 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.33.9 // indirect - github.com/aws/smithy-go v1.22.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.36.0 + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 // indirect + github.com/aws/aws-sdk-go-v2/config v1.29.5 + github.com/aws/aws-sdk-go-v2/credentials v1.17.58 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.58 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.75.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 // indirect + github.com/aws/smithy-go v1.22.2 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect github.com/cloudflare/cloudflare-go v0.114.0 // indirect @@ -152,7 +151,6 @@ require ( github.com/go-jose/go-jose/v4 v4.0.4 // indirect github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect github.com/goccy/go-json v0.10.4 // indirect - github.com/golang-jwt/jwt/v4 v4.5.1 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect @@ -160,11 +158,9 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kr/fs v0.1.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/miekg/dns v1.1.62 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -175,31 +171,31 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/cobra v1.8.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/pflag v1.0.6 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.10.0 // indirect github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1084 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect go.opencensus.io v0.24.0 // indirect gocloud.dev v0.40.0 // indirect - golang.org/x/image v0.23.0 // indirect + golang.org/x/image v0.24.0 // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.34.0 // indirect - golang.org/x/oauth2 v0.25.0 // indirect - golang.org/x/sync v0.10.0 - golang.org/x/sys v0.29.0 // indirect + golang.org/x/oauth2 v0.26.0 // indirect + golang.org/x/sync v0.11.0 + golang.org/x/sys v0.30.0 // indirect golang.org/x/term v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.9.0 golang.org/x/tools v0.29.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/api v0.217.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect - google.golang.org/grpc v1.69.4 // indirect - google.golang.org/protobuf v1.36.3 // indirect + google.golang.org/api v0.219.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489 // indirect + google.golang.org/grpc v1.70.0 // indirect + google.golang.org/protobuf v1.36.4 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.61.9 // indirect + modernc.org/libc v1.61.11 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.8.2 // indirect modernc.org/sqlite v1.34.5 // indirect diff --git a/go.sum b/go.sum index 7dde5dc8..199c33dd 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= -cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cel.dev/expr v0.19.0 h1:lXuo+nDhpyJSpWxpPVi5cPUwzKb+dsdOiw6IreM5yt0= +cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -49,8 +49,6 @@ cloud.google.com/go/storage v1.47.0/go.mod h1:Ks0vP374w0PW6jOUameJbapbQKXqkjGd/O dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= -github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= @@ -90,8 +88,6 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapp github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/sarama v1.30.1/go.mod h1:hGgx05L/DiW8XYBXeJdKIN6V2QUy2H6JqME5VT1NLRw= @@ -212,52 +208,52 @@ github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= -github.com/aws/aws-sdk-go-v2 v1.33.0 h1:Evgm4DI9imD81V0WwD+TN4DCwjUMdc94TrduMLbgZJs= -github.com/aws/aws-sdk-go-v2 v1.33.0/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc= -github.com/aws/aws-sdk-go-v2/config v1.29.1 h1:JZhGawAyZ/EuJeBtbQYnaoftczcb2drR2Iq36Wgz4sQ= -github.com/aws/aws-sdk-go-v2/config v1.29.1/go.mod h1:7bR2YD5euaxBhzt2y/oDkt3uNRb6tjFp98GlTFueRwk= -github.com/aws/aws-sdk-go-v2/credentials v1.17.54 h1:4UmqeOqJPvdvASZWrKlhzpRahAulBfyTJQUaYy4+hEI= -github.com/aws/aws-sdk-go-v2/credentials v1.17.54/go.mod h1:RTdfo0P0hbbTxIhmQrOsC/PquBZGabEPnCaxxKRPSnI= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 h1:5grmdTdMsovn9kPZPI23Hhvp0ZyNm5cRO+IZFIYiAfw= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24/go.mod h1:zqi7TVKTswH3Ozq28PkmBmgzG1tona7mo9G2IJg4Cis= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.52 h1:6kI83R98XOnnyzHv9g9KTYXFawMyeQq8NeEERWMAwJk= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.52/go.mod h1:Juj7unpf3CIrWpEyJZhRJ6rJl9IYX7Hd8HOlwaZq/LE= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28 h1:igORFSiH3bfq4lxKFkTSYDhJEUCYo6C8VKiWJjYwQuQ= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28/go.mod h1:3So8EA/aAYm36L7XIvCVwLa0s5N0P7o2b1oqnx/2R4g= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28 h1:1mOW9zAUMhTSrMDssEHS/ajx8JcAj/IcftzcmNlmVLI= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28/go.mod h1:kGlXVIWDfvt2Ox5zEaNglmq0hXPHgQFNMix33Tw22jA= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.28 h1:7kpeALOUeThs2kEjlAxlADAVfxKmkYAedlpZ3kdoSJ4= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.28/go.mod h1:pyaOYEdp1MJWgtXLy6q80r3DhsVdOIOZNB9hdTcJIvI= +github.com/aws/aws-sdk-go-v2 v1.36.0 h1:b1wM5CcE65Ujwn565qcwgtOTT1aT4ADOHHgglKjG7fk= +github.com/aws/aws-sdk-go-v2 v1.36.0/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 h1:zAxi9p3wsZMIaVCdoiQp2uZ9k1LsZvmAnoTBeZPXom0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8/go.mod h1:3XkePX5dSaxveLAYY7nsbsZZrKxCyEuE5pM4ziFxyGg= +github.com/aws/aws-sdk-go-v2/config v1.29.5 h1:4lS2IB+wwkj5J43Tq/AwvnscBerBJtQQ6YS7puzCI1k= +github.com/aws/aws-sdk-go-v2/config v1.29.5/go.mod h1:SNzldMlDVbN6nWxM7XsUiNXPSa1LWlqiXtvh/1PrJGg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.58 h1:/d7FUpAPU8Lf2KUdjniQvfNdlMID0Sd9pS23FJ3SS9Y= +github.com/aws/aws-sdk-go-v2/credentials v1.17.58/go.mod h1:aVYW33Ow10CyMQGFgC0ptMRIqJWvJ4nxZb0sUiuQT/A= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.58 h1:/BsEGAyMai+KdXS+CMHlLhB5miAO19wOqE6tj8azWPM= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.58/go.mod h1:KHM3lfl/sAJBCoLI1Lsg5w4SD2VDYWwQi7vxbKhw7TI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 h1:lWm9ucLSRFiI4dQQafLrEOmEDGry3Swrz0BIRdiHJqQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31/go.mod h1:Huu6GG0YTfbPphQkDSo4dEGmQRTKb9k9G7RdtyQWxuI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 h1:ACxDklUKKXb48+eg5ROZXi1vDgfMyfIA/WyvqHcHI0o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31/go.mod h1:yadnfsDwqXeVaohbGc/RaD287PuyRw2wugkh5ZL2J6k= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31 h1:8IwBjuLdqIO1dGB+dZ9zJEl8wzY3bVYxcs0Xyu/Lsc0= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31/go.mod h1:8tMBcuVjL4kP/ECEIWTCWtwV2kj6+ouEKl4cqR4iWLw= github.com/aws/aws-sdk-go-v2/service/acm v1.30.13 h1:aPCPsgDxQqOS3zPJKYJQVh02q8stjSQ1haHaUucCAUM= github.com/aws/aws-sdk-go-v2/service/acm v1.30.13/go.mod h1:3pfuOCVLzWu3aiavTB9bOIdZpVadNYt6fyZdp+fDOSU= github.com/aws/aws-sdk-go-v2/service/cloudfront v1.44.5 h1:oBLlEuSL5G9W8M4GtEVdNi+xsQP+9lphVkbYf38Isgs= github.com/aws/aws-sdk-go-v2/service/cloudfront v1.44.5/go.mod h1:H/t3dGwvHy2WJ+ZwyDBWva7ttsoxSxt5qC1OMcc0iJ0= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.2 h1:e6um6+DWYQP1XCa+E9YVtG/9v1qk5lyAOelMOVwSyO8= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.2/go.mod h1:dIW8puxSbYLSPv/ju0d9A3CpwXdtqvJtYKDMVmPLOWE= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9 h1:TQmKDyETFGiXVhZfQ/I0cCFziqqX58pi4tKJGYGFSz0= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9/go.mod h1:HVLPK2iHQBUx7HfZeOQSEu3v2ubZaAY2YPbAm5/WUyY= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.9 h1:2aInXbh02XsbO0KobPGMNXyv2QP73VDKsWPNJARj/+4= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.9/go.mod h1:dgXS1i+HgWnYkPXqNoPIPKeUsUUYHaUbThC90aDnNiE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5 h1:siiQ+jummya9OLPDEyHVb2dLW4aOMe22FGDd0sAfuSw= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5/go.mod h1:iHVx2J9pWzITdP5MJY6qWfG34TfD9EA+Qi3eV6qQCXw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 h1:O+8vD2rGjfihBewr5bT+QUfYUHIxCVgG61LHoT59shM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12/go.mod h1:usVdWJaosa66NMvmCrr08NcWDBRv4E6+YFG2pUdw1Lk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12 h1:tkVNm99nkJnFo1H9IIQb5QkCiPcvCDn3Pos+IeTbGRA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12/go.mod h1:dIVlquSPUMqEJtx2/W17SM2SuESRaVEhEV9alcMqxjw= github.com/aws/aws-sdk-go-v2/service/route53 v1.48.1 h1:njgAP7Rtt4DGdTGFPhJ4gaZXCD1CDj/SZDa5W4ZgSTs= github.com/aws/aws-sdk-go-v2/service/route53 v1.48.1/go.mod h1:TN4PcCL0lvqmYcv+AV8iZFC4Sd0FM06QDaoBXrFEftU= -github.com/aws/aws-sdk-go-v2/service/s3 v1.73.2 h1:F3h8VYq9ZLBXYurmwrT8W0SPhgCcU0q+0WZJfT1dFt0= -github.com/aws/aws-sdk-go-v2/service/s3 v1.73.2/go.mod h1:jGJ/v7FIi7Ys9t54tmEFnrxuaWeJLpwNgKp2DXAVhOU= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.11 h1:kuIyu4fTT38Kj7YCC7ouNbVZSSpqkZ+LzIfhCr6Dg+I= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.11/go.mod h1:Ro744S4fKiCCuZECXgOi760TiYylUM8ZBf6OGiZzJtY= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10 h1:l+dgv/64iVlQ3WsBbnn+JSbkj01jIi+SM0wYsj3y/hY= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10/go.mod h1:Fzsj6lZEb8AkTE5S68OhcbBqeWPsR8RnGuKPr8Todl8= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.9 h1:BRVDbewN6VZcwr+FBOszDKvYeXY1kJ+GGMCcpghlw0U= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.9/go.mod h1:f6vjfZER1M17Fokn0IzssOTMT2N8ZSq+7jnNF0tArvw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.75.3 h1:JBod0SnNqcWQ0+uAyzeRFG1zCHotW8DukumYYyNy0zo= +github.com/aws/aws-sdk-go-v2/service/s3 v1.75.3/go.mod h1:FHSHmyEUkzRbaFFqqm6bkLAOQHgqhsLmfCahvCBMiyA= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 h1:3LXNnmtH3TURctC23hnC0p/39Q5gre3FI7BNOiDcVWc= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.13/go.mod h1:7Yn+p66q/jt38qMoVfNvjbm3D89mGBnkwDcijgtih8w= github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= -github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= +github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/baidubce/bce-sdk-go v0.9.214 h1:bsVfwMh/emI6vreEveUEq9xAr6xtHLycTAGy2K7kvKM= github.com/baidubce/bce-sdk-go v0.9.214/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -305,8 +301,6 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= -github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -431,8 +425,6 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= -github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= @@ -560,8 +552,6 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= -github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= -github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.132 h1:5LqzrJa8LADcY0sDEdV35e8nbwI7RoUQEt+KXWvWoY0= github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.132/go.mod h1:JWz2ujO9X3oU5wb6kXp+DpR2UuDj2SldDbX8T0FSuhI= @@ -601,8 +591,6 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -634,7 +622,6 @@ github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjS github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -649,9 +636,6 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= -github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= @@ -739,8 +723,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU= github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs= -github.com/pocketbase/pocketbase v0.24.4 h1:kw/c23HccoxMV/19U9QlDcvNJgQ66vlUrxGQDZicWKM= -github.com/pocketbase/pocketbase v0.24.4/go.mod h1:EfXV/8RUY76jA6g1RPNHjOuW7wTd2bz0QlvAI/RU8YY= +github.com/pocketbase/pocketbase v0.25.0 h1:/4YQq1hd0muvhzbERyUTVNh88N0BCj5diqK0jtLN6k8= +github.com/pocketbase/pocketbase v0.25.0/go.mod h1:tOtOv7f3vJhAiyUluIwV9JPuKeknZRQ9F6uJE3W/ntI= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/povsister/scp v0.0.0-20240802064259-28781e87b246 h1:c4D8BPWLOxxdaxQLfLKQXH2YXY/E9yo3jrDSL54XrTw= @@ -805,8 +789,9 @@ github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= @@ -964,14 +949,14 @@ 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-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= -golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc= +golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/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= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= -golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= +golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= +golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1056,8 +1041,8 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1071,8 +1056,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1147,8 +1132,8 @@ golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -1172,7 +1157,6 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= @@ -1180,8 +1164,8 @@ golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1269,8 +1253,8 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.217.0 h1:GYrUtD289o4zl1AhiTZL0jvQGa2RDLyC+kX1N/lfGOU= -google.golang.org/api v0.217.0/go.mod h1:qMc2E8cBAbQlRypBTBWHklNJlaZZJBwDv81B1Iu8oSI= +google.golang.org/api v0.219.0 h1:nnKIvxKs/06jWawp2liznTBnMRQBEPpGo7I+oEypTX0= +google.golang.org/api v0.219.0/go.mod h1:K6OmjGm+NtLrIkHxv1U3a0qIf/0JOvAHd5O/6AoyKYE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1313,8 +1297,8 @@ google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489 h1:5bKytslY8ViY0Cj/ewmRtrWHW64bNF03cAatUUFCdFI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1332,8 +1316,8 @@ google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= -google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1349,8 +1333,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= +google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1408,15 +1392,15 @@ k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJ k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.23.13 h1:PFiaemQwE/jdwi8XEHyEV+qYWoIuikLP3T4rvDeJb00= -modernc.org/ccgo/v4 v4.23.13/go.mod h1:vdN4h2WR5aEoNondUx26K7G8X+nuBscYnAEWSRmN2/0= +modernc.org/ccgo/v4 v4.23.15 h1:wFDan71KnYqeHz4eF63vmGE6Q6Pc0PUGDpP0PRMYjDc= +modernc.org/ccgo/v4 v4.23.15/go.mod h1:nJX30dks/IWuBOnVa7VRii9Me4/9TZ1SC9GNtmARTy0= modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= -modernc.org/gc/v2 v2.6.1 h1:+Qf6xdG8l7B27TQ8D8lw/iFMUj1RXRBOuMUWziJOsk8= -modernc.org/gc/v2 v2.6.1/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/libc v1.61.9 h1:PLSBXVkifXGELtJ5BOnBUyAHr7lsatNwFU/RRo4kfJM= -modernc.org/libc v1.61.9/go.mod h1:61xrnzk/aR8gr5bR7Uj/lLFLuXu2/zMpIjcry63Eumk= +modernc.org/gc/v2 v2.6.2 h1:YBXi5Kqp6aCK3fIxwKQ3/fErvawVKwjOLItxj1brGds= +modernc.org/gc/v2 v2.6.2/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.61.11 h1:6sZG8uB6EMMG7iTLPTndi8jyTdgAQNIeLGjCFICACZw= +modernc.org/libc v1.61.11/go.mod h1:HHX+srFdn839oaJRd0W8hBM3eg+mieyZCAjWwB08/nM= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= From 5f5c835533701f0bd41063f8d55bddd545dcdb76 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 6 Feb 2025 13:11:40 +0800 Subject: [PATCH 04/23] feat: add `ExtractCertificatesFromPEM` util func --- internal/applicant/providers.go | 68 +++++++++---------- internal/deployer/providers.go | 56 +++++++-------- .../edgio-applications/edgio_applications.go | 36 ++-------- internal/pkg/utils/certs/extractor.go | 48 +++++++++++++ internal/pkg/utils/certs/parser.go | 1 + internal/pkg/utils/maps/maps.go | 11 ++- internal/pkg/vendors/gname-sdk/client.go | 2 +- 7 files changed, 125 insertions(+), 97 deletions(-) create mode 100644 internal/pkg/utils/certs/extractor.go diff --git a/internal/applicant/providers.go b/internal/applicant/providers.go index f53c7287..d992e9c1 100644 --- a/internal/applicant/providers.go +++ b/internal/applicant/providers.go @@ -35,8 +35,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeACMEHttpReq: { access := domain.AccessConfigForACMEHttpReq{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerACMEHttpReq.NewChallengeProvider(&providerACMEHttpReq.ACMEHttpReqApplicantConfig{ @@ -52,8 +52,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeAliyun, domain.ApplyDNSProviderTypeAliyunDNS: { access := domain.AccessConfigForAliyun{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerAliyun.NewChallengeProvider(&providerAliyun.AliyunApplicantConfig{ @@ -68,8 +68,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeAWS, domain.ApplyDNSProviderTypeAWSRoute53: { access := domain.AccessConfigForAWS{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerAWSRoute53.NewChallengeProvider(&providerAWSRoute53.AWSRoute53ApplicantConfig{ @@ -86,8 +86,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeAzureDNS: { access := domain.AccessConfigForAzure{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerAzureDNS.NewChallengeProvider(&providerAzureDNS.AzureDNSApplicantConfig{ @@ -104,8 +104,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeCloudflare: { access := domain.AccessConfigForCloudflare{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerCloudflare.NewChallengeProvider(&providerCloudflare.CloudflareApplicantConfig{ @@ -119,8 +119,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeClouDNS: { access := domain.AccessConfigForClouDNS{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerClouDNS.NewChallengeProvider(&providerClouDNS.ClouDNSApplicantConfig{ @@ -135,8 +135,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeGname: { access := domain.AccessConfigForGname{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerGname.NewChallengeProvider(&providerGname.GnameApplicantConfig{ @@ -151,8 +151,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeGoDaddy: { access := domain.AccessConfigForGoDaddy{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerGoDaddy.NewChallengeProvider(&providerGoDaddy.GoDaddyApplicantConfig{ @@ -167,8 +167,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeHuaweiCloud, domain.ApplyDNSProviderTypeHuaweiCloudDNS: { access := domain.AccessConfigForHuaweiCloud{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerHuaweiCloud.NewChallengeProvider(&providerHuaweiCloud.HuaweiCloudApplicantConfig{ @@ -184,8 +184,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeNameDotCom: { access := domain.AccessConfigForNameDotCom{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerNameDotCom.NewChallengeProvider(&providerNameDotCom.NameDotComApplicantConfig{ @@ -200,8 +200,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeNameSilo: { access := domain.AccessConfigForNameSilo{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerNameSilo.NewChallengeProvider(&providerNameSilo.NameSiloApplicantConfig{ @@ -215,8 +215,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeNS1: { access := domain.AccessConfigForNS1{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerNS1.NewChallengeProvider(&providerNS1.NS1ApplicantConfig{ @@ -230,8 +230,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypePowerDNS: { access := domain.AccessConfigForPowerDNS{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerPowerDNS.NewChallengeProvider(&providerPowerDNS.PowerDNSApplicantConfig{ @@ -246,8 +246,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeRainYun: { access := domain.AccessConfigForRainYun{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerRainYun.NewChallengeProvider(&providerRainYun.RainYunApplicantConfig{ @@ -261,8 +261,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeTencentCloud, domain.ApplyDNSProviderTypeTencentCloudDNS: { access := domain.AccessConfigForTencentCloud{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerTencentCloud.NewChallengeProvider(&providerTencentCloud.TencentCloudApplicantConfig{ @@ -277,8 +277,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeVolcEngine, domain.ApplyDNSProviderTypeVolcEngineDNS: { access := domain.AccessConfigForVolcEngine{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerVolcEngine.NewChallengeProvider(&providerVolcEngine.VolcEngineApplicantConfig{ @@ -293,8 +293,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeWestcn: { access := domain.AccessConfigForWestcn{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerWestcn.NewChallengeProvider(&providerWestcn.WestcnApplicantConfig{ diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 344c78e6..c5f7f40a 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -54,8 +54,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeAliyunALB, domain.DeployProviderTypeAliyunCDN, domain.DeployProviderTypeAliyunCLB, domain.DeployProviderTypeAliyunDCDN, domain.DeployProviderTypeAliyunLive, domain.DeployProviderTypeAliyunNLB, domain.DeployProviderTypeAliyunOSS, domain.DeployProviderTypeAliyunWAF: { access := domain.AccessConfigForAliyun{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } switch options.Provider { @@ -146,8 +146,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeAWSCloudFront: { access := domain.AccessConfigForAWS{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } switch options.Provider { @@ -168,8 +168,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeBaiduCloudCDN: { access := domain.AccessConfigForBaiduCloud{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } switch options.Provider { @@ -189,8 +189,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeBytePlusCDN: { access := domain.AccessConfigForBytePlus{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } switch options.Provider { @@ -210,8 +210,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeDogeCloudCDN: { access := domain.AccessConfigForDogeCloud{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } deployer, err := providerDogeCDN.NewWithLogger(&providerDogeCDN.DogeCloudCDNDeployerConfig{ @@ -225,8 +225,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeEdgioApplications: { access := domain.AccessConfigForEdgio{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } deployer, err := providerEdgioApplications.NewWithLogger(&providerEdgioApplications.EdgioApplicationsDeployerConfig{ @@ -240,8 +240,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeHuaweiCloudCDN, domain.DeployProviderTypeHuaweiCloudELB: { access := domain.AccessConfigForHuaweiCloud{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } switch options.Provider { @@ -291,8 +291,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeKubernetesSecret: { access := domain.AccessConfigForKubernetes{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } deployer, err := providerK8sSecret.NewWithLogger(&providerK8sSecret.K8sSecretDeployerConfig{ @@ -309,8 +309,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeQiniuCDN, domain.DeployProviderTypeQiniuPili: { access := domain.AccessConfigForQiniu{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } switch options.Provider { @@ -339,8 +339,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeSSH: { access := domain.AccessConfigForSSH{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } deployer, err := providerSSH.NewWithLogger(&providerSSH.SshDeployerConfig{ @@ -367,8 +367,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeTencentCloudCDN, domain.DeployProviderTypeTencentCloudCLB, domain.DeployProviderTypeTencentCloudCOS, domain.DeployProviderTypeTencentCloudCSS, domain.DeployProviderTypeTencentCloudECDN, domain.DeployProviderTypeTencentCloudEO: { access := domain.AccessConfigForTencentCloud{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } switch options.Provider { @@ -435,8 +435,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeUCloudUCDN, domain.DeployProviderTypeUCloudUS3: { access := domain.AccessConfigForUCloud{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } switch options.Provider { @@ -468,8 +468,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeVolcEngineCDN, domain.DeployProviderTypeVolcEngineCLB, domain.DeployProviderTypeVolcEngineDCDN, domain.DeployProviderTypeVolcEngineLive, domain.DeployProviderTypeVolcEngineTOS: { access := domain.AccessConfigForVolcEngine{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } switch options.Provider { @@ -525,8 +525,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeWebhook: { access := domain.AccessConfigForWebhook{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } deployer, err := providerWebhook.NewWithLogger(&providerWebhook.WebhookDeployerConfig{ diff --git a/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go b/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go index d4d70e04..5a6d4bf2 100644 --- a/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go +++ b/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go @@ -2,13 +2,13 @@ import ( "context" - "encoding/pem" "errors" xerrors "github.com/pkg/errors" "github.com/usual2970/certimate/internal/pkg/core/deployer" "github.com/usual2970/certimate/internal/pkg/core/logger" + "github.com/usual2970/certimate/internal/pkg/utils/certs" edgsdk "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7" edgsdkDtos "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7/dtos" ) @@ -57,7 +57,10 @@ func NewWithLogger(config *EdgioApplicationsDeployerConfig, logger logger.Logger func (d *EdgioApplicationsDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) { // 提取 Edgio 所需的服务端证书和中间证书内容 - privateCertPem, intermediateCertPem := extractCertChains(certPem) + privateCertPem, intermediateCertPem, err := certs.ExtractCertificatesFromPEM(certPem) + if err != nil { + return nil, err + } // 上传 TLS 证书 // REF: https://docs.edg.io/rest_api/#tag/tls-certs/operation/postConfigV01TlsCerts @@ -81,32 +84,3 @@ func createSdkClient(clientId, clientSecret string) (*edgsdk.EdgioClient, error) client := edgsdk.NewEdgioClient(clientId, clientSecret, "", "") return client, nil } - -func extractCertChains(certPem string) (primaryCertPem string, intermediateCertPem string) { - pemBlocks := make([]*pem.Block, 0) - pemData := []byte(certPem) - for { - block, rest := pem.Decode(pemData) - if block == nil { - break - } - - pemBlocks = append(pemBlocks, block) - pemData = rest - } - - primaryCertPem = "" - intermediateCertPem = "" - - if len(pemBlocks) > 0 { - primaryCertPem = string(pem.EncodeToMemory(pemBlocks[0])) - } - - if len(pemBlocks) > 1 { - for i := 1; i < len(pemBlocks); i++ { - intermediateCertPem += string(pem.EncodeToMemory(pemBlocks[i])) - } - } - - return primaryCertPem, intermediateCertPem -} diff --git a/internal/pkg/utils/certs/extractor.go b/internal/pkg/utils/certs/extractor.go new file mode 100644 index 00000000..bf07b4f3 --- /dev/null +++ b/internal/pkg/utils/certs/extractor.go @@ -0,0 +1,48 @@ +package certs + +import ( + "encoding/pem" + "errors" +) + +// 从 PEM 编码的证书字符串解析并提取服务器证书和中间证书。 +// +// 入参: +// - certPem: 证书 PEM 内容。 +// +// 出参: +// - serverCertPem: 服务器证书的 PEM 内容。 +// - interCertPem: 中间证书的 PEM 内容。 +// - err: 错误。 +func ExtractCertificatesFromPEM(certPem string) (serverCertPem string, interCertPem string, err error) { + pemBlocks := make([]*pem.Block, 0) + pemData := []byte(certPem) + for { + block, rest := pem.Decode(pemData) + if block == nil || block.Type != "CERTIFICATE" { + break + } + + pemBlocks = append(pemBlocks, block) + pemData = rest + } + + serverCertPem = "" + interCertPem = "" + + if len(pemBlocks) == 0 { + return "", "", errors.New("failed to decode PEM block") + } + + if len(pemBlocks) > 0 { + serverCertPem = string(pem.EncodeToMemory(pemBlocks[0])) + } + + if len(pemBlocks) > 1 { + for i := 1; i < len(pemBlocks); i++ { + interCertPem += string(pem.EncodeToMemory(pemBlocks[i])) + } + } + + return serverCertPem, interCertPem, nil +} diff --git a/internal/pkg/utils/certs/parser.go b/internal/pkg/utils/certs/parser.go index 89338336..af62b03a 100644 --- a/internal/pkg/utils/certs/parser.go +++ b/internal/pkg/utils/certs/parser.go @@ -13,6 +13,7 @@ import ( ) // 从 PEM 编码的证书字符串解析并返回一个 x509.Certificate 对象。 +// PEM 内容可能是包含多张证书的证书链,但只返回第一个证书(即服务器证书)。 // // 入参: // - certPem: 证书 PEM 内容。 diff --git a/internal/pkg/utils/maps/maps.go b/internal/pkg/utils/maps/maps.go index a33d34aa..a88b6629 100644 --- a/internal/pkg/utils/maps/maps.go +++ b/internal/pkg/utils/maps/maps.go @@ -183,7 +183,7 @@ func GetValueOrDefaultAsBool(dict map[string]any, key string, defaultValue bool) return defaultValue } -// 将字典解码为指定类型的结构体。 +// 将字典填充到指定类型的结构体。 // 与 [json.Unmarshal] 类似,但传入的是一个 [map[string]interface{}] 对象而非 JSON 格式的字符串。 // // 入参: @@ -191,8 +191,8 @@ func GetValueOrDefaultAsBool(dict map[string]any, key string, defaultValue bool) // - output: 结构体指针。 // // 出参: -// - 错误信息。如果解码失败,则返回错误信息。 -func Decode(dict map[string]any, output any) error { +// - 错误信息。如果填充失败,则返回错误信息。 +func Populate(dict map[string]any, output any) error { config := &mapstructure.DecoderConfig{ Metadata: nil, Result: output, @@ -207,3 +207,8 @@ func Decode(dict map[string]any, output any) error { return decoder.Decode(dict) } + +// Deprecated: Use [Populate] instead. +func Decode(dict map[string]any, output any) error { + return Populate(dict, output) +} diff --git a/internal/pkg/vendors/gname-sdk/client.go b/internal/pkg/vendors/gname-sdk/client.go index d034cfeb..507555e9 100644 --- a/internal/pkg/vendors/gname-sdk/client.go +++ b/internal/pkg/vendors/gname-sdk/client.go @@ -150,7 +150,7 @@ func (c *GnameClient) sendRequestWithResult(path string, params map[string]any, if err := json.Unmarshal(resp.Body(), &jsonResp); err != nil { return fmt.Errorf("failed to parse response: %w", err) } - if err := maps.Decode(jsonResp, &result); err != nil { + if err := maps.Populate(jsonResp, &result); err != nil { return fmt.Errorf("failed to parse response: %w", err) } From a41ee9c3ca582e01f03f6b9eb558292559adcfc3 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 6 Feb 2025 16:01:46 +0800 Subject: [PATCH 05/23] feat: enhance certificate model --- internal/applicant/acme_user.go | 11 +- internal/applicant/applicant.go | 30 ++-- internal/domain/certificate.go | 92 ++++++++++-- .../deployer/providers/qiniu-cdn/qiniu_cdn.go | 6 +- .../providers/qiniu-sslcert/qiniu_sslcert.go | 2 +- internal/pkg/vendors/qiniu-sdk/client.go | 16 +-- internal/repository/acme_account.go | 38 +++-- internal/repository/certificate.go | 49 +++++++ internal/repository/workflow.go | 53 ++++--- internal/repository/workflow_output.go | 132 ++++++++++-------- .../workflow/node-processor/apply_node.go | 55 +++----- .../workflow/node-processor/condition_node.go | 10 +- .../workflow/node-processor/deploy_node.go | 44 +++--- .../node-processor/execute_failure_node.go | 9 +- .../node-processor/execute_success_node.go | 9 +- internal/workflow/node-processor/processor.go | 5 +- .../workflow/node-processor/start_node.go | 6 +- .../workflow/node-processor/upload_node.go | 59 +++----- migrations/1738767422_updated_certificate.go | 127 +++++++++++++++++ ...flow.go => 1738828775_updated_workflow.go} | 0 ....go => 1738828788_updated_workflow_run.go} | 0 .../certificate/CertificateDetail.tsx | 24 +++- .../workflow/node/UploadNodeConfigForm.tsx | 2 +- ui/src/domain/certificate.ts | 3 + ui/src/i18n/locales/en/nls.certificate.json | 4 + ui/src/i18n/locales/zh/nls.certificate.json | 4 + ui/src/pages/certificates/CertificateList.tsx | 12 +- ui/src/pages/workflows/WorkflowDetail.tsx | 6 +- ui/src/pages/workflows/WorkflowList.tsx | 2 +- ui/src/utils/error.ts | 8 +- 30 files changed, 545 insertions(+), 273 deletions(-) create mode 100644 migrations/1738767422_updated_certificate.go rename migrations/{1737479489_updated_workflow.go => 1738828775_updated_workflow.go} (100%) rename migrations/{1737479538_updated_workflow_run.go => 1738828788_updated_workflow_run.go} (100%) 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"; }; From 24b591ed62462e59f9a5bce0b8424e1c12db4260 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 6 Feb 2025 16:02:17 +0800 Subject: [PATCH 06/23] fix: nil pointer dereference --- internal/workflow/node-processor/apply_node.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index 76f180f0..d600ebb1 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -120,10 +120,12 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo } 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 { - return true, fmt.Sprintf("已申请过证书,且证书尚未临近过期(到期尚余 %d 天,预计距 %d 天时续期)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays) + if lastCertificate != nil { + renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24 + expirationTime := time.Until(lastCertificate.ExpireAt) + if expirationTime > renewalInterval { + return true, fmt.Sprintf("已申请过证书,且证书尚未临近过期(到期尚余 %d 天,预计距 %d 天时续期)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays) + } } } From 4b931f782e16b2c8629ed656ba457da9b97d1e9d Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 6 Feb 2025 16:18:23 +0800 Subject: [PATCH 07/23] refactor(ui): clean code --- ui/src/domain/workflow.ts | 34 ++++++++------------------------- ui/src/stores/workflow/index.ts | 12 ++++++------ 2 files changed, 14 insertions(+), 32 deletions(-) diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index d001f2e9..82625c21 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -276,21 +276,21 @@ export const updateNode = (node: WorkflowNode, targetNode: WorkflowNode) => { }); }; -export const addNode = (node: WorkflowNode, preId: string, targetNode: WorkflowNode) => { +export const addNode = (node: WorkflowNode, previousNodeId: string, targetNode: WorkflowNode) => { return produce(node, (draft) => { let current = draft; while (current) { - if (current.id === preId && targetNode.type !== WorkflowNodeType.Branch && targetNode.type !== WorkflowNodeType.ExecuteResultBranch) { + if (current.id === previousNodeId && targetNode.type !== WorkflowNodeType.Branch && targetNode.type !== WorkflowNodeType.ExecuteResultBranch) { targetNode.next = current.next; current.next = targetNode; break; - } else if (current.id === preId && (targetNode.type === WorkflowNodeType.Branch || targetNode.type === WorkflowNodeType.ExecuteResultBranch)) { + } else if (current.id === previousNodeId && (targetNode.type === WorkflowNodeType.Branch || targetNode.type === WorkflowNodeType.ExecuteResultBranch)) { targetNode.branches![0].next = current.next; current.next = targetNode; break; } if (current.type === WorkflowNodeType.Branch || current.type === WorkflowNodeType.ExecuteResultBranch) { - current.branches = current.branches!.map((branch) => addNode(branch, preId, targetNode)); + current.branches = current.branches!.map((branch) => addNode(branch, previousNodeId, targetNode)); } current = current.next as WorkflowNode; } @@ -382,15 +382,15 @@ export const removeBranch = (node: WorkflowNode, branchNodeId: string, branchInd }); }; -// 1 个分支的节点,不应该能获取到相邻分支上节点的输出 -export const getWorkflowOutputBeforeId = (node: WorkflowNode, id: string, type: string): WorkflowNode[] => { +export const getWorkflowOutputBeforeId = (root: WorkflowNode, nodeId: string, type: string): WorkflowNode[] => { + // 1 个分支的节点,不应该能获取到相邻分支上节点的输出 const output: WorkflowNode[] = []; const traverse = (current: WorkflowNode, output: WorkflowNode[]) => { if (!current) { return false; } - if (current.id === id) { + if (current.id === nodeId) { return true; } @@ -422,7 +422,7 @@ export const getWorkflowOutputBeforeId = (node: WorkflowNode, id: string, type: return traverse(current.next as WorkflowNode, output); }; - traverse(node, output); + traverse(root, output); return output; }; @@ -446,21 +446,3 @@ export const isAllNodesValidated = (node: WorkflowNode): boolean => { return true; }; - -/** - * @deprecated - */ -export const getExecuteMethod = (node: WorkflowNode): { trigger: string; triggerCron: string } => { - if (node.type === WorkflowNodeType.Start) { - const config = node.config as WorkflowNodeConfigForStart; - return { - trigger: config.trigger ?? "", - triggerCron: config.triggerCron ?? "", - }; - } else { - return { - trigger: "", - triggerCron: "", - }; - } -}; diff --git a/ui/src/stores/workflow/index.ts b/ui/src/stores/workflow/index.ts index 4e1559ff..9ec83926 100644 --- a/ui/src/stores/workflow/index.ts +++ b/ui/src/stores/workflow/index.ts @@ -25,14 +25,14 @@ export type WorkflowState = { discard(): void; destroy(): void; - addNode: (node: WorkflowNode, preId: string) => void; + addNode: (node: WorkflowNode, previousNodeId: string) => void; updateNode: (node: WorkflowNode) => void; removeNode: (nodeId: string) => void; addBranch: (branchId: string) => void; removeBranch: (branchId: string, index: number) => void; - getWorkflowOuptutBeforeId: (id: string, type: string) => WorkflowNode[]; + getWorkflowOuptutBeforeId: (nodeId: string, type: string) => WorkflowNode[]; }; export const useWorkflowStore = create((set, get) => ({ @@ -143,10 +143,10 @@ export const useWorkflowStore = create((set, get) => ({ }); }, - addNode: async (node: WorkflowNode, preId: string) => { + addNode: async (node: WorkflowNode, previousNodeId: string) => { if (!get().initialized) throw "Workflow not initialized yet"; - const root = addNode(get().workflow.draft!, preId, node); + const root = addNode(get().workflow.draft!, previousNodeId, node); const resp = await saveWorkflow({ id: get().workflow.id!, draft: root, @@ -243,7 +243,7 @@ export const useWorkflowStore = create((set, get) => ({ }); }, - getWorkflowOuptutBeforeId: (id: string, type: string) => { - return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, id, type); + getWorkflowOuptutBeforeId: (nodeId: string, type: string) => { + return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, nodeId, type); }, })); From 5b9e39a4499bd59164547b3c4ed7679975fbed6b Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 6 Feb 2025 16:39:40 +0800 Subject: [PATCH 08/23] fix: #439 --- internal/applicant/applicant.go | 6 +++--- ui/src/pages/settings/SettingsPassword.tsx | 2 +- ui/src/repository/admin.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go index c565e1ff..5acc8800 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -47,7 +47,7 @@ type applicantOptions struct { DnsPropagationTimeout int32 DnsTTL int32 DisableFollowCNAME bool - ReplacedARIAccount string + ReplacedARIAcctId string ReplacedARICertId string } @@ -93,7 +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.ReplacedARIAcctId = lastCertificate.ACMEAccountUrl options.ReplacedARICertId = replacedARICertId } } @@ -170,7 +170,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap Domains: options.Domains, Bundle: true, } - if options.ReplacedARICertId != "" && options.ReplacedARIAccount != acmeUser.Registration.URI { + if options.ReplacedARICertId != "" && options.ReplacedARIAcctId != acmeUser.Registration.URI { certRequest.ReplacesCertID = options.ReplacedARICertId } certResource, err := client.Certificate.Obtain(certRequest) diff --git a/ui/src/pages/settings/SettingsPassword.tsx b/ui/src/pages/settings/SettingsPassword.tsx index 59447be7..de1cb5ef 100644 --- a/ui/src/pages/settings/SettingsPassword.tsx +++ b/ui/src/pages/settings/SettingsPassword.tsx @@ -34,7 +34,7 @@ const SettingsPassword = () => { onSubmit: async (values) => { try { await authWithPassword(getAuthStore().record!.email, values.oldPassword); - await saveAdmin({ password: values.newPassword }); + await saveAdmin({ password: values.newPassword, passwordConfirm: values.confirmPassword }); messageApi.success(t("common.text.operation_succeeded")); diff --git a/ui/src/repository/admin.ts b/ui/src/repository/admin.ts index 15074fee..1d43eca0 100644 --- a/ui/src/repository/admin.ts +++ b/ui/src/repository/admin.ts @@ -10,7 +10,7 @@ export const getAuthStore = () => { return getPocketBase().authStore; }; -export const save = (data: { email: string } | { password: string }) => { +export const save = (data: { email: string } | { password: string; passwordConfirm: string }) => { return getPocketBase() .collection(COLLECTION_NAME) .update(getAuthStore().record?.id || "", data); From d32fce98ae7f0a3a0fcfec6ba467e2e7791b49f5 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 6 Feb 2025 19:56:41 +0800 Subject: [PATCH 09/23] feat: save related runId in certificates or workflow outputs --- internal/domain/certificate.go | 1 + internal/domain/workflow_output.go | 1 + internal/repository/certificate.go | 2 + internal/repository/workflow_output.go | 9 ++- .../workflow/node-processor/apply_node.go | 9 ++- .../workflow/node-processor/deploy_node.go | 14 ++-- .../workflow/node-processor/notify_node.go | 10 +-- internal/workflow/node-processor/processor.go | 8 ++- .../workflow/node-processor/upload_node.go | 39 ++++++++++- internal/workflow/processor/processor.go | 25 ++++--- internal/workflow/service.go | 2 +- migrations/1738839725_updated_certificate.go | 67 +++++++++++++++++++ .../1738840633_updated_workflow_output.go | 63 +++++++++++++++++ 13 files changed, 215 insertions(+), 35 deletions(-) create mode 100644 migrations/1738839725_updated_certificate.go create mode 100644 migrations/1738840633_updated_workflow_output.go diff --git a/internal/domain/certificate.go b/internal/domain/certificate.go index 1abecb9c..f57a2c4a 100644 --- a/internal/domain/certificate.go +++ b/internal/domain/certificate.go @@ -27,6 +27,7 @@ type Certificate struct { ACMECertStableUrl string `json:"acmeCertStableUrl" db:"acmeCertStableUrl"` WorkflowId string `json:"workflowId" db:"workflowId"` WorkflowNodeId string `json:"workflowNodeId" db:"workflowNodeId"` + WorkflowRunId string `json:"workflowRunId" db:"workflowRunId"` WorkflowOutputId string `json:"workflowOutputId" db:"workflowOutputId"` DeletedAt *time.Time `json:"deleted" db:"deleted"` } diff --git a/internal/domain/workflow_output.go b/internal/domain/workflow_output.go index 44a70bc2..57af169b 100644 --- a/internal/domain/workflow_output.go +++ b/internal/domain/workflow_output.go @@ -5,6 +5,7 @@ const CollectionNameWorkflowOutput = "workflow_output" type WorkflowOutput struct { Meta WorkflowId string `json:"workflowId" db:"workflow"` + RunId string `json:"runId" db:"runId"` NodeId string `json:"nodeId" db:"nodeId"` Node *WorkflowNode `json:"node" db:"node"` Outputs []WorkflowNodeIO `json:"outputs" db:"outputs"` diff --git a/internal/repository/certificate.go b/internal/repository/certificate.go index db0e2b4c..0695ca47 100644 --- a/internal/repository/certificate.go +++ b/internal/repository/certificate.go @@ -112,6 +112,7 @@ func (r *CertificateRepository) Save(ctx context.Context, certificate *domain.Ce record.Set("acmeCertUrl", certificate.ACMECertUrl) record.Set("acmeCertStableUrl", certificate.ACMECertStableUrl) record.Set("workflowId", certificate.WorkflowId) + record.Set("workflowRunId", certificate.WorkflowRunId) record.Set("workflowNodeId", certificate.WorkflowNodeId) record.Set("workflowOutputId", certificate.WorkflowOutputId) if err := app.GetApp().Save(record); err != nil { @@ -149,6 +150,7 @@ func (r *CertificateRepository) castRecordToModel(record *core.Record) (*domain. ACMECertUrl: record.GetString("acmeCertUrl"), ACMECertStableUrl: record.GetString("acmeCertStableUrl"), WorkflowId: record.GetString("workflowId"), + WorkflowRunId: record.GetString("workflowRunId"), WorkflowNodeId: record.GetString("workflowNodeId"), WorkflowOutputId: record.GetString("workflowOutputId"), } diff --git a/internal/repository/workflow_output.go b/internal/repository/workflow_output.go index f5965396..1adf0c8c 100644 --- a/internal/repository/workflow_output.go +++ b/internal/repository/workflow_output.go @@ -40,7 +40,7 @@ func (r *WorkflowOutputRepository) GetByNodeId(ctx context.Context, workflowNode } func (r *WorkflowOutputRepository) Save(ctx context.Context, workflowOutput *domain.WorkflowOutput) (*domain.WorkflowOutput, error) { - record, err := r.saveRecord(ctx, workflowOutput) + record, err := r.saveRecord(workflowOutput) if err != nil { return workflowOutput, err } @@ -52,7 +52,7 @@ func (r *WorkflowOutputRepository) Save(ctx context.Context, workflowOutput *dom } func (r *WorkflowOutputRepository) SaveWithCertificate(ctx context.Context, workflowOutput *domain.WorkflowOutput, certificate *domain.Certificate) (*domain.WorkflowOutput, error) { - record, err := r.saveRecord(ctx, workflowOutput) + record, err := r.saveRecord(workflowOutput) if err != nil { return workflowOutput, err } else { @@ -63,6 +63,7 @@ func (r *WorkflowOutputRepository) SaveWithCertificate(ctx context.Context, work if certificate != nil { certificate.WorkflowId = workflowOutput.WorkflowId + certificate.WorkflowRunId = workflowOutput.RunId certificate.WorkflowNodeId = workflowOutput.NodeId certificate.WorkflowOutputId = workflowOutput.Id certificate, err := NewCertificateRepository().Save(ctx, certificate) @@ -108,6 +109,7 @@ func (r *WorkflowOutputRepository) castRecordToModel(record *core.Record) (*doma UpdatedAt: record.GetDateTime("updated").Time(), }, WorkflowId: record.GetString("workflowId"), + RunId: record.GetString("runId"), NodeId: record.GetString("nodeId"), Node: node, Outputs: outputs, @@ -116,7 +118,7 @@ func (r *WorkflowOutputRepository) castRecordToModel(record *core.Record) (*doma return workflowOutput, nil } -func (r *WorkflowOutputRepository) saveRecord(ctx context.Context, output *domain.WorkflowOutput) (*core.Record, error) { +func (r *WorkflowOutputRepository) saveRecord(output *domain.WorkflowOutput) (*core.Record, error) { collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflowOutput) if err != nil { return nil, err @@ -132,6 +134,7 @@ func (r *WorkflowOutputRepository) saveRecord(ctx context.Context, output *domai } } record.Set("workflowId", output.WorkflowId) + record.Set("runId", output.RunId) record.Set("nodeId", output.NodeId) record.Set("node", output.Node) record.Set("outputs", output.Outputs) diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index d600ebb1..0fdf268f 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -14,18 +14,20 @@ import ( ) type applyNode struct { - node *domain.WorkflowNode + node *domain.WorkflowNode + *nodeLogger + certRepo certificateRepository outputRepo workflowOutputRepository - *nodeLogger } func NewApplyNode(node *domain.WorkflowNode) *applyNode { return &applyNode{ node: node, nodeLogger: NewNodeLogger(node), - outputRepo: repository.NewWorkflowOutputRepository(), + certRepo: repository.NewCertificateRepository(), + outputRepo: repository.NewWorkflowOutputRepository(), } } @@ -81,6 +83,7 @@ func (n *applyNode) Run(ctx context.Context) error { // TODO: 先保持一个节点始终只有一个输出,后续增加版本控制 currentOutput := &domain.WorkflowOutput{ WorkflowId: getContextWorkflowId(ctx), + RunId: getContextWorkflowRunId(ctx), NodeId: n.node.Id, Node: n.node, Succeeded: true, diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go index 28acadb0..f5379519 100644 --- a/internal/workflow/node-processor/deploy_node.go +++ b/internal/workflow/node-processor/deploy_node.go @@ -12,18 +12,20 @@ import ( ) type deployNode struct { - node *domain.WorkflowNode + node *domain.WorkflowNode + *nodeLogger + certRepo certificateRepository outputRepo workflowOutputRepository - *nodeLogger } func NewDeployNode(node *domain.WorkflowNode) *deployNode { return &deployNode{ node: node, nodeLogger: NewNodeLogger(node), - outputRepo: repository.NewWorkflowOutputRepository(), + certRepo: repository.NewCertificateRepository(), + outputRepo: repository.NewWorkflowOutputRepository(), } } @@ -61,7 +63,7 @@ func (n *deployNode) Run(ctx context.Context) error { } // 初始化部署器 - deploy, err := deployer.NewWithDeployNode(n.node, struct { + deployer, err := deployer.NewWithDeployNode(n.node, struct { Certificate string PrivateKey string }{Certificate: certificate.Certificate, PrivateKey: certificate.PrivateKey}) @@ -71,7 +73,7 @@ func (n *deployNode) Run(ctx context.Context) error { } // 部署证书 - if err := deploy.Deploy(ctx); err != nil { + if err := deployer.Deploy(ctx); err != nil { n.AddOutput(ctx, n.node.Name, "部署失败", err.Error()) return err } @@ -80,8 +82,8 @@ func (n *deployNode) Run(ctx context.Context) error { // 保存执行结果 // TODO: 先保持一个节点始终只有一个输出,后续增加版本控制 currentOutput := &domain.WorkflowOutput{ - Meta: domain.Meta{}, WorkflowId: getContextWorkflowId(ctx), + RunId: getContextWorkflowRunId(ctx), NodeId: n.node.Id, Node: n.node, Succeeded: true, diff --git a/internal/workflow/node-processor/notify_node.go b/internal/workflow/node-processor/notify_node.go index 0ba5eb1f..052ebda7 100644 --- a/internal/workflow/node-processor/notify_node.go +++ b/internal/workflow/node-processor/notify_node.go @@ -9,15 +9,17 @@ import ( ) type notifyNode struct { - node *domain.WorkflowNode - settingsRepo settingsRepository + node *domain.WorkflowNode *nodeLogger + + settingsRepo settingsRepository } func NewNotifyNode(node *domain.WorkflowNode) *notifyNode { return ¬ifyNode{ - node: node, - nodeLogger: NewNodeLogger(node), + node: node, + nodeLogger: NewNodeLogger(node), + settingsRepo: repository.NewSettingsRepository(), } } diff --git a/internal/workflow/node-processor/processor.go b/internal/workflow/node-processor/processor.go index bf2b12f4..33e82e3b 100644 --- a/internal/workflow/node-processor/processor.go +++ b/internal/workflow/node-processor/processor.go @@ -10,7 +10,7 @@ import ( type NodeProcessor interface { Run(ctx context.Context) error - Log(ctx context.Context) *domain.WorkflowRunLog + GetLog(ctx context.Context) *domain.WorkflowRunLog AddOutput(ctx context.Context, title, content string, err ...string) } @@ -42,7 +42,7 @@ func NewNodeLogger(node *domain.WorkflowNode) *nodeLogger { } } -func (l *nodeLogger) Log(ctx context.Context) *domain.WorkflowRunLog { +func (l *nodeLogger) GetLog(ctx context.Context) *domain.WorkflowRunLog { return l.log } @@ -84,3 +84,7 @@ func GetProcessor(node *domain.WorkflowNode) (NodeProcessor, error) { func getContextWorkflowId(ctx context.Context) string { return ctx.Value("workflow_id").(string) } + +func getContextWorkflowRunId(ctx context.Context) string { + return ctx.Value("workflow_run_id").(string) +} diff --git a/internal/workflow/node-processor/upload_node.go b/internal/workflow/node-processor/upload_node.go index 7b1908d9..ed9ecd21 100644 --- a/internal/workflow/node-processor/upload_node.go +++ b/internal/workflow/node-processor/upload_node.go @@ -3,6 +3,7 @@ package nodeprocessor import ( "context" "errors" + "strings" "time" "github.com/usual2970/certimate/internal/domain" @@ -11,15 +12,19 @@ import ( ) type uploadNode struct { - node *domain.WorkflowNode - outputRepo workflowOutputRepository + node *domain.WorkflowNode *nodeLogger + + certRepo certificateRepository + outputRepo workflowOutputRepository } func NewUploadNode(node *domain.WorkflowNode) *uploadNode { return &uploadNode{ node: node, nodeLogger: NewNodeLogger(node), + + certRepo: repository.NewCertificateRepository(), outputRepo: repository.NewWorkflowOutputRepository(), } } @@ -38,6 +43,12 @@ func (n *uploadNode) Run(ctx context.Context) error { return err } + // 检测是否可以跳过本次执行 + if skippable, skipReason := n.checkCanSkip(ctx, lastOutput); skippable { + n.AddOutput(ctx, n.node.Name, skipReason) + return nil + } + // 检查证书是否过期 // 如果证书过期,则直接返回错误 certX509, err := certs.ParseCertificateFromPEM(nodeConfig.Certificate) @@ -50,7 +61,7 @@ func (n *uploadNode) Run(ctx context.Context) error { return errors.New("certificate is expired") } - // 生成实体 + // 生成证书实体 certificate := &domain.Certificate{ Source: domain.CertificateSourceTypeUpload, } @@ -60,6 +71,7 @@ func (n *uploadNode) Run(ctx context.Context) error { // TODO: 先保持一个节点始终只有一个输出,后续增加版本控制 currentOutput := &domain.WorkflowOutput{ WorkflowId: getContextWorkflowId(ctx), + RunId: getContextWorkflowRunId(ctx), NodeId: n.node.Id, Node: n.node, Succeeded: true, @@ -76,3 +88,24 @@ func (n *uploadNode) Run(ctx context.Context) error { return nil } + +func (n *uploadNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (skip bool, reason string) { + if lastOutput != nil && lastOutput.Succeeded { + // 比较和上次上传时的关键配置(即影响证书上传的)参数是否一致 + currentNodeConfig := n.node.GetConfigForUpload() + lastNodeConfig := lastOutput.Node.GetConfigForUpload() + if strings.TrimSpace(currentNodeConfig.Certificate) != strings.TrimSpace(lastNodeConfig.Certificate) { + return false, "配置项变化:证书" + } + if strings.TrimSpace(currentNodeConfig.PrivateKey) != strings.TrimSpace(lastNodeConfig.PrivateKey) { + return false, "配置项变化:私钥" + } + + lastCertificate, _ := n.certRepo.GetByWorkflowNodeId(ctx, n.node.Id) + if lastCertificate != nil { + return true, "已上传过证书" + } + } + + return false, "" +} diff --git a/internal/workflow/processor/processor.go b/internal/workflow/processor/processor.go index 47663136..0923a8b2 100644 --- a/internal/workflow/processor/processor.go +++ b/internal/workflow/processor/processor.go @@ -8,24 +8,27 @@ import ( ) type workflowProcessor struct { - workflow *domain.Workflow - logs []domain.WorkflowRunLog + workflow *domain.Workflow + workflowRun *domain.WorkflowRun + workflorRunLogs []domain.WorkflowRunLog } -func NewWorkflowProcessor(workflow *domain.Workflow) *workflowProcessor { +func NewWorkflowProcessor(workflow *domain.Workflow, workflowRun *domain.WorkflowRun) *workflowProcessor { return &workflowProcessor{ - workflow: workflow, - logs: make([]domain.WorkflowRunLog, 0), + workflow: workflow, + workflowRun: workflowRun, + workflorRunLogs: make([]domain.WorkflowRunLog, 0), } } func (w *workflowProcessor) Run(ctx context.Context) error { - ctx = setContextWorkflowId(ctx, w.workflow.Id) + ctx = context.WithValue(ctx, "workflow_id", w.workflow.Id) + ctx = context.WithValue(ctx, "workflow_run_id", w.workflowRun.Id) return w.processNode(ctx, w.workflow.Content) } func (w *workflowProcessor) GetRunLogs() []domain.WorkflowRunLog { - return w.logs + return w.workflorRunLogs } func (w *workflowProcessor) processNode(ctx context.Context, node *domain.WorkflowNode) error { @@ -49,9 +52,9 @@ func (w *workflowProcessor) processNode(ctx context.Context, node *domain.Workfl } runErr = processor.Run(ctx) - log := processor.Log(ctx) + log := processor.GetLog(ctx) if log != nil { - w.logs = append(w.logs, *log) + w.workflorRunLogs = append(w.workflorRunLogs, *log) } if runErr != nil { break @@ -75,10 +78,6 @@ func (w *workflowProcessor) processNode(ctx context.Context, node *domain.Workfl return nil } -func setContextWorkflowId(ctx context.Context, id string) context.Context { - return context.WithValue(ctx, "workflow_id", id) -} - func getBranchByType(branches []domain.WorkflowNode, nodeType domain.WorkflowNodeType) *domain.WorkflowNode { for _, branch := range branches { if branch.Type == nodeType { diff --git a/internal/workflow/service.go b/internal/workflow/service.go index 0a5f2f96..c3b4a3e0 100644 --- a/internal/workflow/service.go +++ b/internal/workflow/service.go @@ -141,7 +141,7 @@ func (s *WorkflowService) runWithData(ctx context.Context, runData *workflowRunD run = resp } - processor := processor.NewWorkflowProcessor(workflow) + processor := processor.NewWorkflowProcessor(workflow, run) if runErr := processor.Run(ctx); runErr != nil { run.Status = domain.WorkflowRunStatusTypeFailed run.EndedAt = time.Now() diff --git a/migrations/1738839725_updated_certificate.go b/migrations/1738839725_updated_certificate.go new file mode 100644 index 00000000..447d5297 --- /dev/null +++ b/migrations/1738839725_updated_certificate.go @@ -0,0 +1,67 @@ +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` + "`" + `)", + "CREATE INDEX ` + "`" + `idx_2cRXqNDyyp` + "`" + ` ON ` + "`" + `certificate` + "`" + ` (` + "`" + `workflowRunId` + "`" + `)" + ] + }`), &collection); err != nil { + return err + } + + // add field + if err := collection.Fields.AddMarshaledJSONAt(15, []byte(`{ + "cascadeDelete": false, + "collectionId": "qjp8lygssgwyqyz", + "hidden": false, + "id": "relation3917999135", + "maxSelect": 1, + "minSelect": 0, + "name": "workflowRunId", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }`)); 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": [ + "CREATE INDEX ` + "`" + `idx_Jx8TXzDCmw` + "`" + ` ON ` + "`" + `certificate` + "`" + ` (` + "`" + `workflowId` + "`" + `)", + "CREATE INDEX ` + "`" + `idx_kcKpgAZapk` + "`" + ` ON ` + "`" + `certificate` + "`" + ` (` + "`" + `workflowNodeId` + "`" + `)" + ] + }`), &collection); err != nil { + return err + } + + // remove field + collection.Fields.RemoveById("relation3917999135") + + return app.Save(collection) + }) +} diff --git a/migrations/1738840633_updated_workflow_output.go b/migrations/1738840633_updated_workflow_output.go new file mode 100644 index 00000000..6e836a76 --- /dev/null +++ b/migrations/1738840633_updated_workflow_output.go @@ -0,0 +1,63 @@ +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("bqnxb95f2cooowp") + if err != nil { + return err + } + + // update collection data + if err := json.Unmarshal([]byte(`{ + "indexes": [ + "CREATE INDEX ` + "`" + `idx_BYoQPsz4my` + "`" + ` ON ` + "`" + `workflow_output` + "`" + ` (` + "`" + `workflowId` + "`" + `)", + "CREATE INDEX ` + "`" + `idx_O9zxLETuxJ` + "`" + ` ON ` + "`" + `workflow_output` + "`" + ` (` + "`" + `runId` + "`" + `)" + ] + }`), &collection); err != nil { + return err + } + + // add field + if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{ + "cascadeDelete": false, + "collectionId": "qjp8lygssgwyqyz", + "hidden": false, + "id": "relation821863227", + "maxSelect": 1, + "minSelect": 0, + "name": "runId", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }`)); err != nil { + return err + } + + return app.Save(collection) + }, func(app core.App) error { + collection, err := app.FindCollectionByNameOrId("bqnxb95f2cooowp") + if err != nil { + return err + } + + // update collection data + if err := json.Unmarshal([]byte(`{ + "indexes": [] + }`), &collection); err != nil { + return err + } + + // remove field + collection.Fields.RemoveById("relation821863227") + + return app.Save(collection) + }) +} From 3f9fda8a2d6456bb63b1da15baec0321b7986a6b Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 6 Feb 2025 20:09:26 +0800 Subject: [PATCH 10/23] feat: support multiple workflow outputs --- internal/workflow/node-processor/apply_node.go | 9 +++------ internal/workflow/node-processor/deploy_node.go | 16 +++++----------- internal/workflow/node-processor/upload_node.go | 8 ++------ 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index 0fdf268f..6b509d73 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -81,7 +81,7 @@ func (n *applyNode) Run(ctx context.Context) error { // 保存执行结果 // TODO: 先保持一个节点始终只有一个输出,后续增加版本控制 - currentOutput := &domain.WorkflowOutput{ + output := &domain.WorkflowOutput{ WorkflowId: getContextWorkflowId(ctx), RunId: getContextWorkflowRunId(ctx), NodeId: n.node.Id, @@ -89,10 +89,7 @@ func (n *applyNode) Run(ctx context.Context) error { Succeeded: true, Outputs: n.node.Outputs, } - if lastOutput != nil { - currentOutput.Id = lastOutput.Id - } - if _, err := n.outputRepo.SaveWithCertificate(ctx, currentOutput, certificate); err != nil { + if _, err := n.outputRepo.SaveWithCertificate(ctx, output, certificate); err != nil { n.AddOutput(ctx, n.node.Name, "保存申请记录失败", err.Error()) return err } @@ -127,7 +124,7 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24 expirationTime := time.Until(lastCertificate.ExpireAt) if expirationTime > renewalInterval { - return true, fmt.Sprintf("已申请过证书,且证书尚未临近过期(到期尚余 %d 天,预计距 %d 天时续期)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays) + return true, fmt.Sprintf("已申请过证书,且证书尚未临近过期(到期尚余 %d 天,预计不足 %d 天时续期)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays) } } } diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go index f5379519..72665419 100644 --- a/internal/workflow/node-processor/deploy_node.go +++ b/internal/workflow/node-processor/deploy_node.go @@ -53,13 +53,11 @@ func (n *deployNode) Run(ctx context.Context) error { } // 检测是否可以跳过本次执行 - if skippable, skipReason := n.checkCanSkip(ctx, lastOutput); skippable { - if certificate.CreatedAt.Before(lastOutput.UpdatedAt) { - n.AddOutput(ctx, n.node.Name, "已部署过且证书未更新") - } else { + if certificate.CreatedAt.Before(lastOutput.UpdatedAt) { + if skippable, skipReason := n.checkCanSkip(ctx, lastOutput); skippable { n.AddOutput(ctx, n.node.Name, skipReason) + return nil } - return nil } // 初始化部署器 @@ -80,18 +78,14 @@ func (n *deployNode) Run(ctx context.Context) error { n.AddOutput(ctx, n.node.Name, "部署成功") // 保存执行结果 - // TODO: 先保持一个节点始终只有一个输出,后续增加版本控制 - currentOutput := &domain.WorkflowOutput{ + output := &domain.WorkflowOutput{ WorkflowId: getContextWorkflowId(ctx), RunId: getContextWorkflowRunId(ctx), NodeId: n.node.Id, Node: n.node, Succeeded: true, } - if lastOutput != nil { - currentOutput.Id = lastOutput.Id - } - if _, err := n.outputRepo.Save(ctx, currentOutput); err != nil { + if _, err := n.outputRepo.Save(ctx, output); err != nil { n.AddOutput(ctx, n.node.Name, "保存部署记录失败", err.Error()) return err } diff --git a/internal/workflow/node-processor/upload_node.go b/internal/workflow/node-processor/upload_node.go index ed9ecd21..09d86a5d 100644 --- a/internal/workflow/node-processor/upload_node.go +++ b/internal/workflow/node-processor/upload_node.go @@ -68,8 +68,7 @@ func (n *uploadNode) Run(ctx context.Context) error { certificate.PopulateFromPEM(nodeConfig.Certificate, nodeConfig.PrivateKey) // 保存执行结果 - // TODO: 先保持一个节点始终只有一个输出,后续增加版本控制 - currentOutput := &domain.WorkflowOutput{ + output := &domain.WorkflowOutput{ WorkflowId: getContextWorkflowId(ctx), RunId: getContextWorkflowRunId(ctx), NodeId: n.node.Id, @@ -77,10 +76,7 @@ func (n *uploadNode) Run(ctx context.Context) error { Succeeded: true, Outputs: n.node.Outputs, } - if lastOutput != nil { - currentOutput.Id = lastOutput.Id - } - if _, err := n.outputRepo.SaveWithCertificate(ctx, currentOutput, certificate); err != nil { + if _, err := n.outputRepo.SaveWithCertificate(ctx, output, certificate); err != nil { n.AddOutput(ctx, n.node.Name, "保存上传记录失败", err.Error()) return err } From 886f166e662cac6b7e93801793e54f58a034935c Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 6 Feb 2025 23:11:16 +0800 Subject: [PATCH 11/23] refactor: clean code --- internal/certificate/service.go | 10 +- internal/domain/dtos/workflow.go | 2 +- internal/repository/workflow.go | 59 --------- internal/repository/workflow_output.go | 22 ++-- internal/repository/workflow_run.go | 117 ++++++++++++++++++ internal/rest/routes/routes.go | 7 +- internal/scheduler/scheduler.go | 3 +- internal/statistics/service.go | 8 +- internal/workflow/event.go | 5 +- .../workflow/node-processor/apply_node.go | 2 +- .../workflow/node-processor/condition_node.go | 2 +- .../workflow/node-processor/deploy_node.go | 2 +- .../node-processor/execute_failure_node.go | 2 +- .../node-processor/execute_success_node.go | 2 +- .../workflow/node-processor/notify_node.go | 2 +- internal/workflow/node-processor/processor.go | 2 +- .../workflow/node-processor/start_node.go | 2 +- .../workflow/node-processor/upload_node.go | 4 +- internal/workflow/processor/processor.go | 38 +++--- internal/workflow/service.go | 110 +++++++++------- .../components/workflow/WorkflowRunDetail.tsx | 52 ++++++++ .../workflow/WorkflowRunDetailDrawer.tsx | 52 +++----- 22 files changed, 311 insertions(+), 194 deletions(-) create mode 100644 internal/repository/workflow_run.go create mode 100644 ui/src/components/workflow/WorkflowRunDetail.tsx diff --git a/internal/certificate/service.go b/internal/certificate/service.go index b8f5fa89..406e183c 100644 --- a/internal/certificate/service.go +++ b/internal/certificate/service.go @@ -30,18 +30,18 @@ type certificateRepository interface { } type CertificateService struct { - repo certificateRepository + certRepo certificateRepository } -func NewCertificateService(repo certificateRepository) *CertificateService { +func NewCertificateService(certRepo certificateRepository) *CertificateService { return &CertificateService{ - repo: repo, + certRepo: certRepo, } } func (s *CertificateService) InitSchedule(ctx context.Context) error { app.GetScheduler().MustAdd("certificateExpireSoonNotify", "0 0 * * *", func() { - certs, err := s.repo.ListExpireSoon(context.Background()) + certs, err := s.certRepo.ListExpireSoon(context.Background()) if err != nil { app.GetLogger().Error("failed to get certificates which expire soon", "err", err) return @@ -60,7 +60,7 @@ func (s *CertificateService) InitSchedule(ctx context.Context) error { } func (s *CertificateService) ArchiveFile(ctx context.Context, req *dtos.CertificateArchiveFileReq) ([]byte, error) { - certificate, err := s.repo.GetById(ctx, req.CertificateId) + certificate, err := s.certRepo.GetById(ctx, req.CertificateId) if err != nil { return nil, err } diff --git a/internal/domain/dtos/workflow.go b/internal/domain/dtos/workflow.go index 9d1f5781..3988220b 100644 --- a/internal/domain/dtos/workflow.go +++ b/internal/domain/dtos/workflow.go @@ -4,7 +4,7 @@ import "github.com/usual2970/certimate/internal/domain" type WorkflowStartRunReq struct { WorkflowId string `json:"-"` - Trigger domain.WorkflowTriggerType `json:"trigger"` + RunTrigger domain.WorkflowTriggerType `json:"trigger"` } type WorkflowCancelRunReq struct { diff --git a/internal/repository/workflow.go b/internal/repository/workflow.go index 738e898e..60d60899 100644 --- a/internal/repository/workflow.go +++ b/internal/repository/workflow.go @@ -95,65 +95,6 @@ func (r *WorkflowRepository) Save(ctx context.Context, workflow *domain.Workflow return workflow, nil } -func (r *WorkflowRepository) SaveRun(ctx context.Context, run *domain.WorkflowRun) (*domain.WorkflowRun, error) { - collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflowRun) - if err != nil { - return run, err - } - - var runRecord *core.Record - if run.Id == "" { - runRecord = core.NewRecord(collection) - } else { - runRecord, err = app.GetApp().FindRecordById(collection, run.Id) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return run, err - } - runRecord = core.NewRecord(collection) - } - } - - err = app.GetApp().RunInTransaction(func(txApp core.App) error { - 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, run.WorkflowId) - if err != nil { - return err - } - - workflowRecord.IgnoreUnchangedFields(true) - 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 - } - - run.Id = runRecord.Id - run.CreatedAt = runRecord.GetDateTime("created").Time() - run.UpdatedAt = runRecord.GetDateTime("updated").Time() - - return nil - }) - if err != nil { - return run, err - } - - return run, nil -} - func (r *WorkflowRepository) castRecordToModel(record *core.Record) (*domain.Workflow, error) { if record == nil { return nil, fmt.Errorf("record is nil") diff --git a/internal/repository/workflow_output.go b/internal/repository/workflow_output.go index 1adf0c8c..e75b2cb7 100644 --- a/internal/repository/workflow_output.go +++ b/internal/repository/workflow_output.go @@ -94,12 +94,12 @@ func (r *WorkflowOutputRepository) castRecordToModel(record *core.Record) (*doma node := &domain.WorkflowNode{} if err := record.UnmarshalJSONField("node", node); err != nil { - return nil, errors.New("failed to unmarshal node") + return nil, err } outputs := make([]domain.WorkflowNodeIO, 0) if err := record.UnmarshalJSONField("outputs", &outputs); err != nil { - return nil, errors.New("failed to unmarshal output") + return nil, err } workflowOutput := &domain.WorkflowOutput{ @@ -118,27 +118,27 @@ func (r *WorkflowOutputRepository) castRecordToModel(record *core.Record) (*doma return workflowOutput, nil } -func (r *WorkflowOutputRepository) saveRecord(output *domain.WorkflowOutput) (*core.Record, error) { +func (r *WorkflowOutputRepository) saveRecord(workflowOutput *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 == "" { + if workflowOutput.Id == "" { record = core.NewRecord(collection) } else { - record, err = app.GetApp().FindRecordById(collection, output.Id) + record, err = app.GetApp().FindRecordById(collection, workflowOutput.Id) if err != nil { return record, err } } - record.Set("workflowId", output.WorkflowId) - record.Set("runId", output.RunId) - record.Set("nodeId", output.NodeId) - record.Set("node", output.Node) - record.Set("outputs", output.Outputs) - record.Set("succeeded", output.Succeeded) + record.Set("workflowId", workflowOutput.WorkflowId) + record.Set("runId", workflowOutput.RunId) + record.Set("nodeId", workflowOutput.NodeId) + record.Set("node", workflowOutput.Node) + record.Set("outputs", workflowOutput.Outputs) + record.Set("succeeded", workflowOutput.Succeeded) if err := app.GetApp().Save(record); err != nil { return record, err } diff --git a/internal/repository/workflow_run.go b/internal/repository/workflow_run.go new file mode 100644 index 00000000..01185a45 --- /dev/null +++ b/internal/repository/workflow_run.go @@ -0,0 +1,117 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "fmt" + + "github.com/pocketbase/pocketbase/core" + "github.com/usual2970/certimate/internal/app" + "github.com/usual2970/certimate/internal/domain" +) + +type WorkflowRunRepository struct{} + +func NewWorkflowRunRepository() *WorkflowRunRepository { + return &WorkflowRunRepository{} +} + +func (r *WorkflowRunRepository) GetById(ctx context.Context, id string) (*domain.WorkflowRun, error) { + record, err := app.GetApp().FindRecordById(domain.CollectionNameWorkflowRun, id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, domain.ErrRecordNotFound + } + return nil, err + } + + return r.castRecordToModel(record) +} + +func (r *WorkflowRunRepository) Save(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error) { + collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflowRun) + if err != nil { + return workflowRun, err + } + + var record *core.Record + if workflowRun.Id == "" { + record = core.NewRecord(collection) + } else { + record, err = app.GetApp().FindRecordById(collection, workflowRun.Id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return workflowRun, err + } + record = core.NewRecord(collection) + } + } + + err = app.GetApp().RunInTransaction(func(txApp core.App) error { + record.Set("workflowId", workflowRun.WorkflowId) + record.Set("trigger", string(workflowRun.Trigger)) + record.Set("status", string(workflowRun.Status)) + record.Set("startedAt", workflowRun.StartedAt) + record.Set("endedAt", workflowRun.EndedAt) + record.Set("logs", workflowRun.Logs) + record.Set("error", workflowRun.Error) + err = txApp.Save(record) + if err != nil { + return err + } + + workflowRun.Id = record.Id + workflowRun.CreatedAt = record.GetDateTime("created").Time() + workflowRun.UpdatedAt = record.GetDateTime("updated").Time() + + // 事务级联更新所属工作流的最后运行记录 + workflowRecord, err := txApp.FindRecordById(domain.CollectionNameWorkflow, workflowRun.WorkflowId) + if err != nil { + return err + } else if workflowRecord.GetDateTime("lastRunTime").Time().IsZero() || workflowRun.StartedAt.After(workflowRecord.GetDateTime("lastRunTime").Time()) { + workflowRecord.IgnoreUnchangedFields(true) + workflowRecord.Set("lastRunId", record.Id) + workflowRecord.Set("lastRunStatus", record.GetString("status")) + workflowRecord.Set("lastRunTime", record.GetString("startedAt")) + err = txApp.Save(workflowRecord) + if err != nil { + return err + } + } + + return nil + }) + if err != nil { + return workflowRun, err + } + + return workflowRun, nil +} + +func (r *WorkflowRunRepository) castRecordToModel(record *core.Record) (*domain.WorkflowRun, error) { + if record == nil { + return nil, fmt.Errorf("record is nil") + } + + logs := make([]domain.WorkflowRunLog, 0) + if err := record.UnmarshalJSONField("logs", &logs); err != nil { + return nil, err + } + + workflowRun := &domain.WorkflowRun{ + Meta: domain.Meta{ + Id: record.Id, + CreatedAt: record.GetDateTime("created").Time(), + UpdatedAt: record.GetDateTime("updated").Time(), + }, + WorkflowId: record.GetString("workflowId"), + Status: domain.WorkflowRunStatusType(record.GetString("status")), + Trigger: domain.WorkflowTriggerType(record.GetString("trigger")), + StartedAt: record.GetDateTime("startedAt").Time(), + EndedAt: record.GetDateTime("endedAt").Time(), + Logs: logs, + Error: record.GetString("error"), + } + return workflowRun, nil +} diff --git a/internal/rest/routes/routes.go b/internal/rest/routes/routes.go index 58b4f0f1..756760da 100644 --- a/internal/rest/routes/routes.go +++ b/internal/rest/routes/routes.go @@ -27,13 +27,14 @@ func Register(router *router.Router[*core.RequestEvent]) { certificateSvc = certificate.NewCertificateService(certificateRepo) workflowRepo := repository.NewWorkflowRepository() - workflowSvc = workflow.NewWorkflowService(workflowRepo) + workflowRunRepo := repository.NewWorkflowRunRepository() + workflowSvc = workflow.NewWorkflowService(workflowRepo, workflowRunRepo) statisticsRepo := repository.NewStatisticsRepository() statisticsSvc = statistics.NewStatisticsService(statisticsRepo) - notifyRepo := repository.NewSettingsRepository() - notifySvc = notify.NewNotifyService(notifyRepo) + settingsRepo := repository.NewSettingsRepository() + notifySvc = notify.NewNotifyService(settingsRepo) group := router.Group("/api") group.Bind(apis.RequireSuperuserAuth()) diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go index 055beddb..f5029599 100644 --- a/internal/scheduler/scheduler.go +++ b/internal/scheduler/scheduler.go @@ -8,7 +8,8 @@ import ( func Register() { workflowRepo := repository.NewWorkflowRepository() - workflowSvc := workflow.NewWorkflowService(workflowRepo) + workflowRunRepo := repository.NewWorkflowRunRepository() + workflowSvc := workflow.NewWorkflowService(workflowRepo, workflowRunRepo) certificateRepo := repository.NewCertificateRepository() certificateSvc := certificate.NewCertificateService(certificateRepo) diff --git a/internal/statistics/service.go b/internal/statistics/service.go index 3b1f5876..44388ba9 100644 --- a/internal/statistics/service.go +++ b/internal/statistics/service.go @@ -11,15 +11,15 @@ type statisticsRepository interface { } type StatisticsService struct { - repo statisticsRepository + statRepo statisticsRepository } -func NewStatisticsService(repo statisticsRepository) *StatisticsService { +func NewStatisticsService(statRepo statisticsRepository) *StatisticsService { return &StatisticsService{ - repo: repo, + statRepo: statRepo, } } func (s *StatisticsService) Get(ctx context.Context) (*domain.Statistics, error) { - return s.repo.Get(ctx) + return s.statRepo.Get(ctx) } diff --git a/internal/workflow/event.go b/internal/workflow/event.go index f8117dbc..fa6b4b1a 100644 --- a/internal/workflow/event.go +++ b/internal/workflow/event.go @@ -65,9 +65,10 @@ func onWorkflowRecordCreateOrUpdate(ctx context.Context, record *core.Record) er // 反之,重新添加定时任务 err := scheduler.Add(fmt.Sprintf("workflow#%s", workflowId), record.GetString("triggerCron"), func() { - NewWorkflowService(repository.NewWorkflowRepository()).StartRun(ctx, &dtos.WorkflowStartRunReq{ + workflowSrv := NewWorkflowService(repository.NewWorkflowRepository(), repository.NewWorkflowRunRepository()) + workflowSrv.StartRun(ctx, &dtos.WorkflowStartRunReq{ WorkflowId: workflowId, - Trigger: domain.WorkflowTriggerTypeAuto, + RunTrigger: domain.WorkflowTriggerTypeAuto, }) }) if err != nil { diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index 6b509d73..39a0167f 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -31,7 +31,7 @@ func NewApplyNode(node *domain.WorkflowNode) *applyNode { } } -func (n *applyNode) Run(ctx context.Context) error { +func (n *applyNode) Process(ctx context.Context) error { n.AddOutput(ctx, n.node.Name, "开始执行") // 查询上次执行结果 diff --git a/internal/workflow/node-processor/condition_node.go b/internal/workflow/node-processor/condition_node.go index a511ce20..994965ba 100644 --- a/internal/workflow/node-processor/condition_node.go +++ b/internal/workflow/node-processor/condition_node.go @@ -18,7 +18,7 @@ func NewConditionNode(node *domain.WorkflowNode) *conditionNode { } } -func (n *conditionNode) Run(ctx context.Context) error { +func (n *conditionNode) Process(ctx context.Context) error { // 此类型节点不需要执行任何操作,直接返回 n.AddOutput(ctx, n.node.Name, "完成") diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go index 72665419..dccdf0e8 100644 --- a/internal/workflow/node-processor/deploy_node.go +++ b/internal/workflow/node-processor/deploy_node.go @@ -29,7 +29,7 @@ func NewDeployNode(node *domain.WorkflowNode) *deployNode { } } -func (n *deployNode) Run(ctx context.Context) error { +func (n *deployNode) Process(ctx context.Context) error { n.AddOutput(ctx, n.node.Name, "开始执行") // 查询上次执行结果 diff --git a/internal/workflow/node-processor/execute_failure_node.go b/internal/workflow/node-processor/execute_failure_node.go index 84042a4b..a64019bb 100644 --- a/internal/workflow/node-processor/execute_failure_node.go +++ b/internal/workflow/node-processor/execute_failure_node.go @@ -18,7 +18,7 @@ func NewExecuteFailureNode(node *domain.WorkflowNode) *executeFailureNode { } } -func (n *executeFailureNode) Run(ctx context.Context) error { +func (n *executeFailureNode) Process(ctx context.Context) error { // 此类型节点不需要执行任何操作,直接返回 n.AddOutput(ctx, n.node.Name, "进入执行失败分支") diff --git a/internal/workflow/node-processor/execute_success_node.go b/internal/workflow/node-processor/execute_success_node.go index ef058b06..e0cfea1e 100644 --- a/internal/workflow/node-processor/execute_success_node.go +++ b/internal/workflow/node-processor/execute_success_node.go @@ -18,7 +18,7 @@ func NewExecuteSuccessNode(node *domain.WorkflowNode) *executeSuccessNode { } } -func (n *executeSuccessNode) Run(ctx context.Context) error { +func (n *executeSuccessNode) Process(ctx context.Context) error { // 此类型节点不需要执行任何操作,直接返回 n.AddOutput(ctx, n.node.Name, "进入执行成功分支") diff --git a/internal/workflow/node-processor/notify_node.go b/internal/workflow/node-processor/notify_node.go index 052ebda7..21e6ac15 100644 --- a/internal/workflow/node-processor/notify_node.go +++ b/internal/workflow/node-processor/notify_node.go @@ -24,7 +24,7 @@ func NewNotifyNode(node *domain.WorkflowNode) *notifyNode { } } -func (n *notifyNode) Run(ctx context.Context) error { +func (n *notifyNode) Process(ctx context.Context) error { n.AddOutput(ctx, n.node.Name, "开始执行") nodeConfig := n.node.GetConfigForNotify() diff --git a/internal/workflow/node-processor/processor.go b/internal/workflow/node-processor/processor.go index 33e82e3b..61155892 100644 --- a/internal/workflow/node-processor/processor.go +++ b/internal/workflow/node-processor/processor.go @@ -9,7 +9,7 @@ import ( ) type NodeProcessor interface { - Run(ctx context.Context) error + Process(ctx context.Context) error GetLog(ctx context.Context) *domain.WorkflowRunLog AddOutput(ctx context.Context, title, content string, err ...string) } diff --git a/internal/workflow/node-processor/start_node.go b/internal/workflow/node-processor/start_node.go index 2f1026ad..99e15af2 100644 --- a/internal/workflow/node-processor/start_node.go +++ b/internal/workflow/node-processor/start_node.go @@ -18,7 +18,7 @@ func NewStartNode(node *domain.WorkflowNode) *startNode { } } -func (n *startNode) Run(ctx context.Context) error { +func (n *startNode) Process(ctx context.Context) error { // 此类型节点不需要执行任何操作,直接返回 n.AddOutput(ctx, n.node.Name, "完成") diff --git a/internal/workflow/node-processor/upload_node.go b/internal/workflow/node-processor/upload_node.go index 09d86a5d..9e316501 100644 --- a/internal/workflow/node-processor/upload_node.go +++ b/internal/workflow/node-processor/upload_node.go @@ -29,9 +29,7 @@ func NewUploadNode(node *domain.WorkflowNode) *uploadNode { } } -// Run 上传证书节点执行 -// 包含上传证书的工作流,理论上应该手动执行,如果每天定时执行,也只是重新保存一下 -func (n *uploadNode) Run(ctx context.Context) error { +func (n *uploadNode) Process(ctx context.Context) error { n.AddOutput(ctx, n.node.Name, "进入上传证书节点") nodeConfig := n.node.GetConfigForUpload() diff --git a/internal/workflow/processor/processor.go b/internal/workflow/processor/processor.go index 0923a8b2..39486419 100644 --- a/internal/workflow/processor/processor.go +++ b/internal/workflow/processor/processor.go @@ -8,27 +8,29 @@ import ( ) type workflowProcessor struct { - workflow *domain.Workflow - workflowRun *domain.WorkflowRun - workflorRunLogs []domain.WorkflowRunLog + workflowId string + workflowContent *domain.WorkflowNode + runId string + runLogs []domain.WorkflowRunLog } -func NewWorkflowProcessor(workflow *domain.Workflow, workflowRun *domain.WorkflowRun) *workflowProcessor { +func NewWorkflowProcessor(workflowId string, workflowContent *domain.WorkflowNode, workflowRunId string) *workflowProcessor { return &workflowProcessor{ - workflow: workflow, - workflowRun: workflowRun, - workflorRunLogs: make([]domain.WorkflowRunLog, 0), + workflowId: workflowId, + workflowContent: workflowContent, + runId: workflowRunId, + runLogs: make([]domain.WorkflowRunLog, 0), } } -func (w *workflowProcessor) Run(ctx context.Context) error { - ctx = context.WithValue(ctx, "workflow_id", w.workflow.Id) - ctx = context.WithValue(ctx, "workflow_run_id", w.workflowRun.Id) - return w.processNode(ctx, w.workflow.Content) +func (w *workflowProcessor) Process(ctx context.Context) error { + ctx = context.WithValue(ctx, "workflow_id", w.workflowId) + ctx = context.WithValue(ctx, "workflow_run_id", w.runId) + return w.processNode(ctx, w.workflowContent) } -func (w *workflowProcessor) GetRunLogs() []domain.WorkflowRunLog { - return w.workflorRunLogs +func (w *workflowProcessor) GetLogs() []domain.WorkflowRunLog { + return w.runLogs } func (w *workflowProcessor) processNode(ctx context.Context, node *domain.WorkflowNode) error { @@ -51,10 +53,10 @@ func (w *workflowProcessor) processNode(ctx context.Context, node *domain.Workfl break } - runErr = processor.Run(ctx) + runErr = processor.Process(ctx) log := processor.GetLog(ctx) if log != nil { - w.workflorRunLogs = append(w.workflorRunLogs, *log) + w.runLogs = append(w.runLogs, *log) } if runErr != nil { break @@ -67,9 +69,9 @@ func (w *workflowProcessor) processNode(ctx context.Context, node *domain.Workfl if runErr != nil && current.Next != nil && current.Next.Type != domain.WorkflowNodeTypeExecuteResultBranch { return runErr } else if runErr != nil && current.Next != nil && current.Next.Type == domain.WorkflowNodeTypeExecuteResultBranch { - current = getBranchByType(current.Next.Branches, domain.WorkflowNodeTypeExecuteFailure) + current = w.getBranchByType(current.Next.Branches, domain.WorkflowNodeTypeExecuteFailure) } else if runErr == nil && current.Next != nil && current.Next.Type == domain.WorkflowNodeTypeExecuteResultBranch { - current = getBranchByType(current.Next.Branches, domain.WorkflowNodeTypeExecuteSuccess) + current = w.getBranchByType(current.Next.Branches, domain.WorkflowNodeTypeExecuteSuccess) } else { current = current.Next } @@ -78,7 +80,7 @@ func (w *workflowProcessor) processNode(ctx context.Context, node *domain.Workfl return nil } -func getBranchByType(branches []domain.WorkflowNode, nodeType domain.WorkflowNodeType) *domain.WorkflowNode { +func (w *workflowProcessor) getBranchByType(branches []domain.WorkflowNode, nodeType domain.WorkflowNodeType) *domain.WorkflowNode { for _, branch := range branches { if branch.Type == nodeType { return &branch diff --git a/internal/workflow/service.go b/internal/workflow/service.go index c3b4a3e0..8181c969 100644 --- a/internal/workflow/service.go +++ b/internal/workflow/service.go @@ -13,46 +13,55 @@ import ( processor "github.com/usual2970/certimate/internal/workflow/processor" ) -const defaultRoutines = 10 +const defaultRoutines = 16 type workflowRunData struct { - Workflow *domain.Workflow - RunTrigger domain.WorkflowTriggerType + WorkflowId string + WorkflowContent *domain.WorkflowNode + RunTrigger domain.WorkflowTriggerType } type workflowRepository interface { ListEnabledAuto(ctx context.Context) ([]*domain.Workflow, error) GetById(ctx context.Context, id string) (*domain.Workflow, error) Save(ctx context.Context, workflow *domain.Workflow) (*domain.Workflow, error) - SaveRun(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error) +} + +type workflowRunRepository interface { + GetById(ctx context.Context, id string) (*domain.WorkflowRun, error) + Save(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error) } type WorkflowService struct { ch chan *workflowRunData - repo workflowRepository wg sync.WaitGroup cancel context.CancelFunc + + workflowRepo workflowRepository + workflowRunRepo workflowRunRepository } -func NewWorkflowService(repo workflowRepository) *WorkflowService { - srv := &WorkflowService{ - repo: repo, - ch: make(chan *workflowRunData, 1), - } - +func NewWorkflowService(workflowRepo workflowRepository, workflowRunRepo workflowRunRepository) *WorkflowService { ctx, cancel := context.WithCancel(context.Background()) - srv.cancel = cancel + + srv := &WorkflowService{ + ch: make(chan *workflowRunData, 1), + cancel: cancel, + + workflowRepo: workflowRepo, + workflowRunRepo: workflowRunRepo, + } srv.wg.Add(defaultRoutines) for i := 0; i < defaultRoutines; i++ { - go srv.run(ctx) + go srv.startRun(ctx) } return srv } func (s *WorkflowService) InitSchedule(ctx context.Context) error { - workflows, err := s.repo.ListEnabledAuto(ctx) + workflows, err := s.workflowRepo.ListEnabledAuto(ctx) if err != nil { return err } @@ -62,7 +71,7 @@ func (s *WorkflowService) InitSchedule(ctx context.Context) error { err := scheduler.Add(fmt.Sprintf("workflow#%s", workflow.Id), workflow.TriggerCron, func() { s.StartRun(ctx, &dtos.WorkflowStartRunReq{ WorkflowId: workflow.Id, - Trigger: domain.WorkflowTriggerTypeAuto, + RunTrigger: domain.WorkflowTriggerTypeAuto, }) }) if err != nil { @@ -75,35 +84,50 @@ func (s *WorkflowService) InitSchedule(ctx context.Context) error { } func (s *WorkflowService) StartRun(ctx context.Context, req *dtos.WorkflowStartRunReq) error { - workflow, err := s.repo.GetById(ctx, req.WorkflowId) + workflow, err := s.workflowRepo.GetById(ctx, req.WorkflowId) if err != nil { app.GetLogger().Error("failed to get workflow", "id", req.WorkflowId, "err", err) return err } - if workflow.LastRunStatus == domain.WorkflowRunStatusTypeRunning { - return errors.New("workflow is running") - } - - workflow.LastRunTime = time.Now() - workflow.LastRunStatus = domain.WorkflowRunStatusTypePending - workflow.LastRunId = "" - if resp, err := s.repo.Save(ctx, workflow); err != nil { - return err - } else { - workflow = resp + if workflow.LastRunStatus == domain.WorkflowRunStatusTypePending || workflow.LastRunStatus == domain.WorkflowRunStatusTypeRunning { + return errors.New("workflow is already pending or running") } s.ch <- &workflowRunData{ - Workflow: workflow, - RunTrigger: req.Trigger, + WorkflowId: workflow.Id, + WorkflowContent: workflow.Content, + RunTrigger: req.RunTrigger, } return nil } func (s *WorkflowService) CancelRun(ctx context.Context, req *dtos.WorkflowCancelRunReq) error { + workflow, err := s.workflowRepo.GetById(ctx, req.WorkflowId) + if err != nil { + app.GetLogger().Error("failed to get workflow", "id", req.WorkflowId, "err", err) + return err + } + + workflowRun, err := s.workflowRunRepo.GetById(ctx, req.RunId) + if err != nil { + app.GetLogger().Error("failed to get workflow run", "id", req.RunId, "err", err) + return err + } else if workflowRun.WorkflowId != workflow.Id { + return errors.New("workflow run not found") + } else if workflowRun.Status != domain.WorkflowRunStatusTypePending && workflowRun.Status != domain.WorkflowRunStatusTypeRunning { + return errors.New("workflow run is not pending or running") + } + // TODO: 取消运行,防止因为某些原因意外挂起(如进程被杀死)导致工作流一直处于 running 状态无法重新运行 + // workflowRun.Status = domain.WorkflowRunStatusTypeCanceled + // workflowRun.EndedAt = time.Now() + // if _, err := s.workflowRunRepo.Save(ctx, workflowRun); err != nil { + // return err + // } + + // return nil return errors.New("TODO: 尚未实现") } @@ -113,13 +137,14 @@ func (s *WorkflowService) Stop(ctx context.Context) { s.wg.Wait() } -func (s *WorkflowService) run(ctx context.Context) { +func (s *WorkflowService) startRun(ctx context.Context) { defer s.wg.Done() + for { select { case data := <-s.ch: - if err := s.runWithData(ctx, data); err != nil { - app.GetLogger().Error("failed to run workflow", "id", data.Workflow.Id, "err", err) + if err := s.startRunWithData(ctx, data); err != nil { + app.GetLogger().Error("failed to run workflow", "id", data.WorkflowId, "err", err) } case <-ctx.Done(): return @@ -127,27 +152,26 @@ func (s *WorkflowService) run(ctx context.Context) { } } -func (s *WorkflowService) runWithData(ctx context.Context, runData *workflowRunData) error { - workflow := runData.Workflow +func (s *WorkflowService) startRunWithData(ctx context.Context, data *workflowRunData) error { run := &domain.WorkflowRun{ - WorkflowId: workflow.Id, + WorkflowId: data.WorkflowId, Status: domain.WorkflowRunStatusTypeRunning, - Trigger: runData.RunTrigger, + Trigger: data.RunTrigger, StartedAt: time.Now(), } - if resp, err := s.repo.SaveRun(ctx, run); err != nil { + if resp, err := s.workflowRunRepo.Save(ctx, run); err != nil { return err } else { run = resp } - processor := processor.NewWorkflowProcessor(workflow, run) - if runErr := processor.Run(ctx); runErr != nil { + processor := processor.NewWorkflowProcessor(data.WorkflowId, data.WorkflowContent, run.Id) + if runErr := processor.Process(ctx); runErr != nil { run.Status = domain.WorkflowRunStatusTypeFailed run.EndedAt = time.Now() - run.Logs = processor.GetRunLogs() + run.Logs = processor.GetLogs() run.Error = runErr.Error() - if _, err := s.repo.SaveRun(ctx, run); err != nil { + if _, err := s.workflowRunRepo.Save(ctx, run); err != nil { app.GetLogger().Error("failed to save workflow run", "err", err) } @@ -155,14 +179,14 @@ func (s *WorkflowService) runWithData(ctx context.Context, runData *workflowRunD } run.EndedAt = time.Now() - run.Logs = processor.GetRunLogs() + run.Logs = processor.GetLogs() run.Error = domain.WorkflowRunLogs(run.Logs).ErrorString() if run.Error == "" { run.Status = domain.WorkflowRunStatusTypeSucceeded } else { run.Status = domain.WorkflowRunStatusTypeFailed } - if _, err := s.repo.SaveRun(ctx, run); err != nil { + if _, err := s.workflowRunRepo.Save(ctx, run); err != nil { app.GetLogger().Error("failed to save workflow run", "err", err) return err } diff --git a/ui/src/components/workflow/WorkflowRunDetail.tsx b/ui/src/components/workflow/WorkflowRunDetail.tsx new file mode 100644 index 00000000..db9610f7 --- /dev/null +++ b/ui/src/components/workflow/WorkflowRunDetail.tsx @@ -0,0 +1,52 @@ +import { useTranslation } from "react-i18next"; +import { Alert, Typography } from "antd"; +import dayjs from "dayjs"; + +import Show from "@/components/Show"; +import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun"; + +export type WorkflowRunDetailProps = { + className?: string; + style?: React.CSSProperties; + data: WorkflowRunModel; +}; + +const WorkflowRunDetail = ({ data, ...props }: WorkflowRunDetailProps) => { + const { t } = useTranslation(); + + return ( +
+ + {t("workflow_run.props.status.succeeded")}} /> + + + + {t("workflow_run.props.status.failed")}} /> + + +
+
+ {data.logs?.map((item, i) => { + return ( +
+
{item.nodeName}
+
+ {item.outputs?.map((output, j) => { + return ( +
+
[{dayjs(output.time).format("YYYY-MM-DD HH:mm:ss")}]
+ {output.error ?
{output.error}
:
{output.content}
} +
+ ); + })} +
+
+ ); + })} +
+
+
+ ); +}; + +export default WorkflowRunDetail; diff --git a/ui/src/components/workflow/WorkflowRunDetailDrawer.tsx b/ui/src/components/workflow/WorkflowRunDetailDrawer.tsx index 11e2847c..44ee8be8 100644 --- a/ui/src/components/workflow/WorkflowRunDetailDrawer.tsx +++ b/ui/src/components/workflow/WorkflowRunDetailDrawer.tsx @@ -1,12 +1,12 @@ -import { useTranslation } from "react-i18next"; import { useControllableValue } from "ahooks"; -import { Alert, Drawer, Typography } from "antd"; -import dayjs from "dayjs"; +import { Drawer } from "antd"; import Show from "@/components/Show"; -import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun"; +import { type WorkflowRunModel } from "@/domain/workflowRun"; import { useTriggerElement } from "@/hooks"; +import WorkflowRunDetail from "./WorkflowRunDetail"; + export type WorkflowRunDetailDrawerProps = { data?: WorkflowRunModel; loading?: boolean; @@ -16,8 +16,6 @@ export type WorkflowRunDetailDrawerProps = { }; const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowRunDetailDrawerProps) => { - const { t } = useTranslation(); - const [open, setOpen] = useControllableValue(props, { valuePropName: "open", defaultValuePropName: "defaultOpen", @@ -30,37 +28,19 @@ const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowR <> {triggerEl} - setOpen(false)}> + setOpen(false)} + > - - {t("workflow_run.props.status.succeeded")}} /> - - - - {t("workflow_run.props.status.failed")}} /> - - -
-
- {data!.logs?.map((item, i) => { - return ( -
-
{item.nodeName}
-
- {item.outputs?.map((output, j) => { - return ( -
-
[{dayjs(output.time).format("YYYY-MM-DD HH:mm:ss")}]
- {output.error ?
{output.error}
:
{output.content}
} -
- ); - })} -
-
- ); - })} -
-
+
From b9e28db08902306d2cb37e3c005014a108031abf Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Sat, 8 Feb 2025 21:30:35 +0800 Subject: [PATCH 12/23] fix: nil pointer dereference --- internal/workflow/node-processor/deploy_node.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go index dccdf0e8..e3b6137f 100644 --- a/internal/workflow/node-processor/deploy_node.go +++ b/internal/workflow/node-processor/deploy_node.go @@ -53,7 +53,7 @@ func (n *deployNode) Process(ctx context.Context) error { } // 检测是否可以跳过本次执行 - if certificate.CreatedAt.Before(lastOutput.UpdatedAt) { + if lastOutput != nil && certificate.CreatedAt.Before(lastOutput.UpdatedAt) { if skippable, skipReason := n.checkCanSkip(ctx, lastOutput); skippable { n.AddOutput(ctx, n.node.Name, skipReason) return nil From 0bc40fd6766625660a0bfeedb482eeae052ca389 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Sat, 8 Feb 2025 22:44:13 +0800 Subject: [PATCH 13/23] feat: workflow run dispatcher --- internal/repository/workflow_run.go | 7 + internal/rest/handlers/workflow.go | 2 +- internal/rest/routes/routes.go | 2 +- internal/workflow/dispatcher/dispatcher.go | 274 ++++++++++++++++++ internal/workflow/dispatcher/invoker.go | 104 +++++++ internal/workflow/dispatcher/singleton.go | 31 ++ .../workflow/node-processor/apply_node.go | 3 +- internal/workflow/processor/processor.go | 90 ------ internal/workflow/service.go | 147 +++------- ui/src/components/workflow/WorkflowRuns.tsx | 4 +- ui/src/pages/dashboard/Dashboard.tsx | 5 +- ui/src/pages/workflows/WorkflowDetail.tsx | 25 +- ui/src/pages/workflows/WorkflowList.tsx | 4 +- 13 files changed, 472 insertions(+), 226 deletions(-) create mode 100644 internal/workflow/dispatcher/dispatcher.go create mode 100644 internal/workflow/dispatcher/invoker.go create mode 100644 internal/workflow/dispatcher/singleton.go delete mode 100644 internal/workflow/processor/processor.go diff --git a/internal/repository/workflow_run.go b/internal/repository/workflow_run.go index 01185a45..b1a5234b 100644 --- a/internal/repository/workflow_run.go +++ b/internal/repository/workflow_run.go @@ -69,6 +69,13 @@ func (r *WorkflowRunRepository) Save(ctx context.Context, workflowRun *domain.Wo workflowRecord, err := txApp.FindRecordById(domain.CollectionNameWorkflow, workflowRun.WorkflowId) if err != nil { return err + } else if workflowRun.Id == workflowRecord.GetString("lastRunId") { + workflowRecord.IgnoreUnchangedFields(true) + workflowRecord.Set("lastRunStatus", record.GetString("status")) + err = txApp.Save(workflowRecord) + if err != nil { + return err + } } else if workflowRecord.GetDateTime("lastRunTime").Time().IsZero() || workflowRun.StartedAt.After(workflowRecord.GetDateTime("lastRunTime").Time()) { workflowRecord.IgnoreUnchangedFields(true) workflowRecord.Set("lastRunId", record.Id) diff --git a/internal/rest/handlers/workflow.go b/internal/rest/handlers/workflow.go index 83b3302b..bad474f0 100644 --- a/internal/rest/handlers/workflow.go +++ b/internal/rest/handlers/workflow.go @@ -13,7 +13,7 @@ import ( type workflowService interface { StartRun(ctx context.Context, req *dtos.WorkflowStartRunReq) error CancelRun(ctx context.Context, req *dtos.WorkflowCancelRunReq) error - Stop(ctx context.Context) + Shutdown(ctx context.Context) } type WorkflowHandler struct { diff --git a/internal/rest/routes/routes.go b/internal/rest/routes/routes.go index 756760da..87fcb297 100644 --- a/internal/rest/routes/routes.go +++ b/internal/rest/routes/routes.go @@ -46,6 +46,6 @@ func Register(router *router.Router[*core.RequestEvent]) { func Unregister() { if workflowSvc != nil { - workflowSvc.Stop(context.Background()) + workflowSvc.Shutdown(context.Background()) } } diff --git a/internal/workflow/dispatcher/dispatcher.go b/internal/workflow/dispatcher/dispatcher.go new file mode 100644 index 00000000..0ecc0828 --- /dev/null +++ b/internal/workflow/dispatcher/dispatcher.go @@ -0,0 +1,274 @@ +package dispatcher + +import ( + "context" + "errors" + "fmt" + "os" + "strconv" + "sync" + "time" + + "github.com/usual2970/certimate/internal/app" + "github.com/usual2970/certimate/internal/domain" + "github.com/usual2970/certimate/internal/pkg/utils/slices" +) + +var maxWorkers = 16 + +func init() { + envMaxWorkers := os.Getenv("CERTIMATE_WORKFLOW_MAX_WORKERS") + if n, err := strconv.Atoi(envMaxWorkers); err != nil && n > 0 { + maxWorkers = n + } +} + +type workflowWorker struct { + Data *WorkflowWorkerData + Cancel context.CancelFunc +} + +type WorkflowWorkerData struct { + WorkflowId string + WorkflowContent *domain.WorkflowNode + RunId string +} + +type WorkflowDispatcher struct { + semaphore chan struct{} + + queue []*WorkflowWorkerData + queueMutex sync.Mutex + + workers map[string]*workflowWorker // key: WorkflowId + workerIdMap map[string]string // key: RunId, value: WorkflowId + workerMutex sync.Mutex + + chWork chan *WorkflowWorkerData + chCandi chan struct{} + + wg sync.WaitGroup + + workflowRepo workflowRepository + workflowRunRepo workflowRunRepository +} + +func newWorkflowDispatcher(workflowRepo workflowRepository, workflowRunRepo workflowRunRepository) *WorkflowDispatcher { + dispatcher := &WorkflowDispatcher{ + semaphore: make(chan struct{}, maxWorkers), + + queue: make([]*WorkflowWorkerData, 0), + queueMutex: sync.Mutex{}, + + workers: make(map[string]*workflowWorker), + workerIdMap: make(map[string]string), + workerMutex: sync.Mutex{}, + + chWork: make(chan *WorkflowWorkerData), + chCandi: make(chan struct{}, 1), + + workflowRepo: workflowRepo, + workflowRunRepo: workflowRunRepo, + } + + go func() { + for { + select { + case <-dispatcher.chWork: + dispatcher.dequeueWorker() + + case <-dispatcher.chCandi: + dispatcher.dequeueWorker() + } + } + }() + + return dispatcher +} + +func (w *WorkflowDispatcher) Dispatch(data *WorkflowWorkerData) { + if data == nil { + panic("worker data is nil") + } + + w.enqueueWorker(data) + select { + case w.chWork <- data: + default: + } +} + +func (w *WorkflowDispatcher) Cancel(runId string) { + hasWorker := false + + // 取消正在执行的 WorkflowRun + w.workerMutex.Lock() + if workflowId, ok := w.workerIdMap[runId]; ok { + if worker, ok := w.workers[workflowId]; ok { + hasWorker = true + worker.Cancel() + delete(w.workers, workflowId) + delete(w.workerIdMap, runId) + } + } + w.workerMutex.Unlock() + + // 移除排队中的 WorkflowRun + w.queueMutex.Lock() + w.queue = slices.Filter(w.queue, func(d *WorkflowWorkerData) bool { + return d.RunId != runId + }) + w.queueMutex.Unlock() + + // 已挂起,查询 WorkflowRun 并更新其状态为 Canceled + if !hasWorker { + if run, err := w.workflowRunRepo.GetById(context.Background(), runId); err == nil { + if run.Status == domain.WorkflowRunStatusTypePending || run.Status == domain.WorkflowRunStatusTypeRunning { + run.Status = domain.WorkflowRunStatusTypeCanceled + w.workflowRunRepo.Save(context.Background(), run) + } + } + } +} + +func (w *WorkflowDispatcher) Shutdown() { + // 清空排队中的 WorkflowRun + w.queueMutex.Lock() + w.queue = make([]*WorkflowWorkerData, 0) + w.queueMutex.Unlock() + + // 等待所有正在执行的 WorkflowRun 完成 + w.wg.Wait() + w.workers = make(map[string]*workflowWorker) + w.workerIdMap = make(map[string]string) +} + +func (w *WorkflowDispatcher) enqueueWorker(data *WorkflowWorkerData) { + w.queueMutex.Lock() + defer w.queueMutex.Unlock() + w.queue = append(w.queue, data) +} + +func (w *WorkflowDispatcher) dequeueWorker() { + for { + select { + case w.semaphore <- struct{}{}: + default: + // 达到最大并发数 + return + } + + w.queueMutex.Lock() + if len(w.queue) == 0 { + w.queueMutex.Unlock() + <-w.semaphore + return + } + + data := w.queue[0] + w.queue = w.queue[1:] + w.queueMutex.Unlock() + + // 检查是否有相同 WorkflowId 的 WorkflowRun 正在执行 + // 如果有,则重新排队,以保证同一个工作流同一时间内只有一个正在执行 + // 即不同 WorkflowId 的任务并行化,相同 WorkflowId 的任务串行化 + w.workerMutex.Lock() + if _, exists := w.workers[data.WorkflowId]; exists { + w.queueMutex.Lock() + w.queue = append(w.queue, data) + w.queueMutex.Unlock() + w.workerMutex.Unlock() + + <-w.semaphore + + continue + } + + ctx, cancel := context.WithCancel(context.Background()) + w.workers[data.WorkflowId] = &workflowWorker{data, cancel} + w.workerIdMap[data.RunId] = data.WorkflowId + w.workerMutex.Unlock() + + w.wg.Add(1) + go w.work(ctx, data) + } +} + +func (w *WorkflowDispatcher) work(ctx context.Context, data *WorkflowWorkerData) { + defer func() { + <-w.semaphore + w.workerMutex.Lock() + delete(w.workers, data.WorkflowId) + delete(w.workerIdMap, data.RunId) + w.workerMutex.Unlock() + + w.wg.Done() + + // 尝试取出排队中的其他 WorkflowRun 继续执行 + select { + case w.chCandi <- struct{}{}: + default: + } + }() + + // 查询 WorkflowRun + run, err := w.workflowRunRepo.GetById(ctx, data.RunId) + if err != nil { + if !(errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) { + app.GetLogger().Error(fmt.Sprintf("failed to get workflow run #%s", data.RunId), "err", err) + } + return + } else if run.Status != domain.WorkflowRunStatusTypePending { + return + } else if ctx.Err() != nil { + run.Status = domain.WorkflowRunStatusTypeCanceled + w.workflowRunRepo.Save(ctx, run) + return + } + + // 更新 WorkflowRun 状态为 Running + run.Status = domain.WorkflowRunStatusTypeRunning + if _, err := w.workflowRunRepo.Save(ctx, run); err != nil { + if !(errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) { + panic(err) + } + return + } + + // 执行工作流 + invoker := newWorkflowInvoker(data) + if runErr := invoker.Invoke(ctx); runErr != nil { + if errors.Is(runErr, context.Canceled) { + run.Status = domain.WorkflowRunStatusTypeCanceled + run.Logs = invoker.GetLogs() + } else { + run.Status = domain.WorkflowRunStatusTypeFailed + run.EndedAt = time.Now() + run.Logs = invoker.GetLogs() + run.Error = runErr.Error() + } + + if _, err := w.workflowRunRepo.Save(ctx, run); err != nil { + if !(errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) { + panic(err) + } + } + + return + } + + // 更新 WorkflowRun 状态为 Succeeded/Failed + run.EndedAt = time.Now() + run.Logs = invoker.GetLogs() + run.Error = domain.WorkflowRunLogs(invoker.GetLogs()).ErrorString() + if run.Error == "" { + run.Status = domain.WorkflowRunStatusTypeSucceeded + } else { + run.Status = domain.WorkflowRunStatusTypeFailed + } + if _, err := w.workflowRunRepo.Save(ctx, run); err != nil { + if !(errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) { + panic(err) + } + } +} diff --git a/internal/workflow/dispatcher/invoker.go b/internal/workflow/dispatcher/invoker.go new file mode 100644 index 00000000..3033314e --- /dev/null +++ b/internal/workflow/dispatcher/invoker.go @@ -0,0 +1,104 @@ +package dispatcher + +import ( + "context" + "errors" + + "github.com/usual2970/certimate/internal/domain" + nodes "github.com/usual2970/certimate/internal/workflow/node-processor" +) + +type workflowInvoker struct { + workflowId string + workflowContent *domain.WorkflowNode + runId string + runLogs []domain.WorkflowRunLog +} + +func newWorkflowInvoker(data *WorkflowWorkerData) *workflowInvoker { + if data == nil { + panic("worker data is nil") + } + + return &workflowInvoker{ + workflowId: data.WorkflowId, + workflowContent: data.WorkflowContent, + runId: data.RunId, + runLogs: make([]domain.WorkflowRunLog, 0), + } +} + +func (w *workflowInvoker) Invoke(ctx context.Context) error { + ctx = context.WithValue(ctx, "workflow_id", w.workflowId) + ctx = context.WithValue(ctx, "workflow_run_id", w.runId) + return w.processNode(ctx, w.workflowContent) +} + +func (w *workflowInvoker) GetLogs() []domain.WorkflowRunLog { + return w.runLogs +} + +func (w *workflowInvoker) processNode(ctx context.Context, node *domain.WorkflowNode) error { + current := node + for current != nil { + if ctx.Err() != nil { + return ctx.Err() + } + + if current.Type == domain.WorkflowNodeTypeBranch || current.Type == domain.WorkflowNodeTypeExecuteResultBranch { + for _, branch := range current.Branches { + if err := w.processNode(ctx, &branch); err != nil { + // 并行分支的某一分支发生错误时,忽略此错误,继续执行其他分支 + if !(errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) { + continue + } + return err + } + } + } + + var processor nodes.NodeProcessor + var procErr error + for { + if current.Type != domain.WorkflowNodeTypeBranch && current.Type != domain.WorkflowNodeTypeExecuteResultBranch { + processor, procErr = nodes.GetProcessor(current) + if procErr != nil { + break + } + + procErr = processor.Process(ctx) + log := processor.GetLog(ctx) + if log != nil { + w.runLogs = append(w.runLogs, *log) + } + if procErr != nil { + break + } + } + + break + } + + // TODO: 优化可读性 + if procErr != nil && current.Next != nil && current.Next.Type != domain.WorkflowNodeTypeExecuteResultBranch { + return procErr + } else if procErr != nil && current.Next != nil && current.Next.Type == domain.WorkflowNodeTypeExecuteResultBranch { + current = w.getBranchByType(current.Next.Branches, domain.WorkflowNodeTypeExecuteFailure) + } else if procErr == nil && current.Next != nil && current.Next.Type == domain.WorkflowNodeTypeExecuteResultBranch { + current = w.getBranchByType(current.Next.Branches, domain.WorkflowNodeTypeExecuteSuccess) + } else { + current = current.Next + } + } + + return nil +} + +func (w *workflowInvoker) getBranchByType(branches []domain.WorkflowNode, nodeType domain.WorkflowNodeType) *domain.WorkflowNode { + for _, branch := range branches { + if branch.Type == nodeType { + return &branch + } + } + return nil +} diff --git a/internal/workflow/dispatcher/singleton.go b/internal/workflow/dispatcher/singleton.go new file mode 100644 index 00000000..37c34f56 --- /dev/null +++ b/internal/workflow/dispatcher/singleton.go @@ -0,0 +1,31 @@ +package dispatcher + +import ( + "context" + "sync" + + "github.com/usual2970/certimate/internal/domain" +) + +type workflowRepository interface { + GetById(ctx context.Context, id string) (*domain.Workflow, error) + Save(ctx context.Context, workflow *domain.Workflow) (*domain.Workflow, error) +} + +type workflowRunRepository interface { + GetById(ctx context.Context, id string) (*domain.WorkflowRun, error) + Save(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error) +} + +var ( + instance *WorkflowDispatcher + intanceOnce sync.Once +) + +func GetSingletonDispatcher(workflowRepo workflowRepository, workflowRunRepo workflowRunRepository) *WorkflowDispatcher { + intanceOnce.Do(func() { + instance = newWorkflowDispatcher(workflowRepo, workflowRunRepo) + }) + + return instance +} diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index 39a0167f..d75486ea 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -80,7 +80,6 @@ func (n *applyNode) Process(ctx context.Context) error { certificate.PopulateFromX509(certX509) // 保存执行结果 - // TODO: 先保持一个节点始终只有一个输出,后续增加版本控制 output := &domain.WorkflowOutput{ WorkflowId: getContextWorkflowId(ctx), RunId: getContextWorkflowRunId(ctx), @@ -124,7 +123,7 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24 expirationTime := time.Until(lastCertificate.ExpireAt) if expirationTime > renewalInterval { - return true, fmt.Sprintf("已申请过证书,且证书尚未临近过期(到期尚余 %d 天,预计不足 %d 天时续期)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays) + return true, fmt.Sprintf("已申请过证书,且证书尚未临近过期(尚余 %d 天过期,不足 %d 天时续期)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays) } } } diff --git a/internal/workflow/processor/processor.go b/internal/workflow/processor/processor.go deleted file mode 100644 index 39486419..00000000 --- a/internal/workflow/processor/processor.go +++ /dev/null @@ -1,90 +0,0 @@ -package processor - -import ( - "context" - - "github.com/usual2970/certimate/internal/domain" - nodes "github.com/usual2970/certimate/internal/workflow/node-processor" -) - -type workflowProcessor struct { - workflowId string - workflowContent *domain.WorkflowNode - runId string - runLogs []domain.WorkflowRunLog -} - -func NewWorkflowProcessor(workflowId string, workflowContent *domain.WorkflowNode, workflowRunId string) *workflowProcessor { - return &workflowProcessor{ - workflowId: workflowId, - workflowContent: workflowContent, - runId: workflowRunId, - runLogs: make([]domain.WorkflowRunLog, 0), - } -} - -func (w *workflowProcessor) Process(ctx context.Context) error { - ctx = context.WithValue(ctx, "workflow_id", w.workflowId) - ctx = context.WithValue(ctx, "workflow_run_id", w.runId) - return w.processNode(ctx, w.workflowContent) -} - -func (w *workflowProcessor) GetLogs() []domain.WorkflowRunLog { - return w.runLogs -} - -func (w *workflowProcessor) processNode(ctx context.Context, node *domain.WorkflowNode) error { - current := node - for current != nil { - if current.Type == domain.WorkflowNodeTypeBranch || current.Type == domain.WorkflowNodeTypeExecuteResultBranch { - for _, branch := range current.Branches { - if err := w.processNode(ctx, &branch); err != nil { - continue - } - } - } - - var processor nodes.NodeProcessor - var runErr error - for { - if current.Type != domain.WorkflowNodeTypeBranch && current.Type != domain.WorkflowNodeTypeExecuteResultBranch { - processor, runErr = nodes.GetProcessor(current) - if runErr != nil { - break - } - - runErr = processor.Process(ctx) - log := processor.GetLog(ctx) - if log != nil { - w.runLogs = append(w.runLogs, *log) - } - if runErr != nil { - break - } - } - - break - } - - if runErr != nil && current.Next != nil && current.Next.Type != domain.WorkflowNodeTypeExecuteResultBranch { - return runErr - } else if runErr != nil && current.Next != nil && current.Next.Type == domain.WorkflowNodeTypeExecuteResultBranch { - current = w.getBranchByType(current.Next.Branches, domain.WorkflowNodeTypeExecuteFailure) - } else if runErr == nil && current.Next != nil && current.Next.Type == domain.WorkflowNodeTypeExecuteResultBranch { - current = w.getBranchByType(current.Next.Branches, domain.WorkflowNodeTypeExecuteSuccess) - } else { - current = current.Next - } - } - - return nil -} - -func (w *workflowProcessor) getBranchByType(branches []domain.WorkflowNode, nodeType domain.WorkflowNodeType) *domain.WorkflowNode { - for _, branch := range branches { - if branch.Type == nodeType { - return &branch - } - } - return nil -} diff --git a/internal/workflow/service.go b/internal/workflow/service.go index 8181c969..2d0a224d 100644 --- a/internal/workflow/service.go +++ b/internal/workflow/service.go @@ -4,23 +4,14 @@ import ( "context" "errors" "fmt" - "sync" "time" "github.com/usual2970/certimate/internal/app" "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/domain/dtos" - processor "github.com/usual2970/certimate/internal/workflow/processor" + "github.com/usual2970/certimate/internal/workflow/dispatcher" ) -const defaultRoutines = 16 - -type workflowRunData struct { - WorkflowId string - WorkflowContent *domain.WorkflowNode - RunTrigger domain.WorkflowTriggerType -} - type workflowRepository interface { ListEnabledAuto(ctx context.Context) ([]*domain.Workflow, error) GetById(ctx context.Context, id string) (*domain.Workflow, error) @@ -33,30 +24,19 @@ type workflowRunRepository interface { } type WorkflowService struct { - ch chan *workflowRunData - wg sync.WaitGroup - cancel context.CancelFunc + dispatcher *dispatcher.WorkflowDispatcher workflowRepo workflowRepository workflowRunRepo workflowRunRepository } func NewWorkflowService(workflowRepo workflowRepository, workflowRunRepo workflowRunRepository) *WorkflowService { - ctx, cancel := context.WithCancel(context.Background()) - srv := &WorkflowService{ - ch: make(chan *workflowRunData, 1), - cancel: cancel, + dispatcher: dispatcher.GetSingletonDispatcher(workflowRepo, workflowRunRepo), workflowRepo: workflowRepo, workflowRunRepo: workflowRunRepo, } - - srv.wg.Add(defaultRoutines) - for i := 0; i < defaultRoutines; i++ { - go srv.startRun(ctx) - } - return srv } @@ -75,7 +55,6 @@ func (s *WorkflowService) InitSchedule(ctx context.Context) error { }) }) if err != nil { - app.GetLogger().Error("failed to add schedule", "err", err) return err } } @@ -86,7 +65,6 @@ func (s *WorkflowService) InitSchedule(ctx context.Context) error { func (s *WorkflowService) StartRun(ctx context.Context, req *dtos.WorkflowStartRunReq) error { workflow, err := s.workflowRepo.GetById(ctx, req.WorkflowId) if err != nil { - app.GetLogger().Error("failed to get workflow", "id", req.WorkflowId, "err", err) return err } @@ -94,69 +72,10 @@ func (s *WorkflowService) StartRun(ctx context.Context, req *dtos.WorkflowStartR return errors.New("workflow is already pending or running") } - s.ch <- &workflowRunData{ - WorkflowId: workflow.Id, - WorkflowContent: workflow.Content, - RunTrigger: req.RunTrigger, - } - - return nil -} - -func (s *WorkflowService) CancelRun(ctx context.Context, req *dtos.WorkflowCancelRunReq) error { - workflow, err := s.workflowRepo.GetById(ctx, req.WorkflowId) - if err != nil { - app.GetLogger().Error("failed to get workflow", "id", req.WorkflowId, "err", err) - return err - } - - workflowRun, err := s.workflowRunRepo.GetById(ctx, req.RunId) - if err != nil { - app.GetLogger().Error("failed to get workflow run", "id", req.RunId, "err", err) - return err - } else if workflowRun.WorkflowId != workflow.Id { - return errors.New("workflow run not found") - } else if workflowRun.Status != domain.WorkflowRunStatusTypePending && workflowRun.Status != domain.WorkflowRunStatusTypeRunning { - return errors.New("workflow run is not pending or running") - } - - // TODO: 取消运行,防止因为某些原因意外挂起(如进程被杀死)导致工作流一直处于 running 状态无法重新运行 - // workflowRun.Status = domain.WorkflowRunStatusTypeCanceled - // workflowRun.EndedAt = time.Now() - // if _, err := s.workflowRunRepo.Save(ctx, workflowRun); err != nil { - // return err - // } - - // return nil - - return errors.New("TODO: 尚未实现") -} - -func (s *WorkflowService) Stop(ctx context.Context) { - s.cancel() - s.wg.Wait() -} - -func (s *WorkflowService) startRun(ctx context.Context) { - defer s.wg.Done() - - for { - select { - case data := <-s.ch: - if err := s.startRunWithData(ctx, data); err != nil { - app.GetLogger().Error("failed to run workflow", "id", data.WorkflowId, "err", err) - } - case <-ctx.Done(): - return - } - } -} - -func (s *WorkflowService) startRunWithData(ctx context.Context, data *workflowRunData) error { run := &domain.WorkflowRun{ - WorkflowId: data.WorkflowId, - Status: domain.WorkflowRunStatusTypeRunning, - Trigger: data.RunTrigger, + WorkflowId: workflow.Id, + Status: domain.WorkflowRunStatusTypePending, + Trigger: req.RunTrigger, StartedAt: time.Now(), } if resp, err := s.workflowRunRepo.Save(ctx, run); err != nil { @@ -165,31 +84,35 @@ func (s *WorkflowService) startRunWithData(ctx context.Context, data *workflowRu run = resp } - processor := processor.NewWorkflowProcessor(data.WorkflowId, data.WorkflowContent, run.Id) - if runErr := processor.Process(ctx); runErr != nil { - run.Status = domain.WorkflowRunStatusTypeFailed - run.EndedAt = time.Now() - run.Logs = processor.GetLogs() - run.Error = runErr.Error() - if _, err := s.workflowRunRepo.Save(ctx, run); err != nil { - app.GetLogger().Error("failed to save workflow run", "err", err) - } - - return fmt.Errorf("failed to run workflow: %w", runErr) - } - - run.EndedAt = time.Now() - run.Logs = processor.GetLogs() - run.Error = domain.WorkflowRunLogs(run.Logs).ErrorString() - if run.Error == "" { - run.Status = domain.WorkflowRunStatusTypeSucceeded - } else { - run.Status = domain.WorkflowRunStatusTypeFailed - } - if _, err := s.workflowRunRepo.Save(ctx, run); err != nil { - app.GetLogger().Error("failed to save workflow run", "err", err) - return err - } + s.dispatcher.Dispatch(&dispatcher.WorkflowWorkerData{ + WorkflowId: workflow.Id, + WorkflowContent: workflow.Content, + RunId: run.Id, + }) return nil } + +func (s *WorkflowService) CancelRun(ctx context.Context, req *dtos.WorkflowCancelRunReq) error { + workflow, err := s.workflowRepo.GetById(ctx, req.WorkflowId) + if err != nil { + return err + } + + workflowRun, err := s.workflowRunRepo.GetById(ctx, req.RunId) + if err != nil { + return err + } else if workflowRun.WorkflowId != workflow.Id { + return errors.New("workflow run not found") + } else if workflowRun.Status != domain.WorkflowRunStatusTypePending && workflowRun.Status != domain.WorkflowRunStatusTypeRunning { + return errors.New("workflow run is not pending or running") + } + + s.dispatcher.Cancel(workflowRun.Id) + + return nil +} + +func (s *WorkflowService) Shutdown(ctx context.Context) { + s.dispatcher.Shutdown() +} diff --git a/ui/src/components/workflow/WorkflowRuns.tsx b/ui/src/components/workflow/WorkflowRuns.tsx index c90ab5a3..e670e102 100644 --- a/ui/src/components/workflow/WorkflowRuns.tsx +++ b/ui/src/components/workflow/WorkflowRuns.tsx @@ -5,9 +5,9 @@ import { ClockCircleOutlined as ClockCircleOutlinedIcon, CloseCircleOutlined as CloseCircleOutlinedIcon, DeleteOutlined as DeleteOutlinedIcon, - PauseCircleOutlined as PauseCircleOutlinedIcon, PauseOutlined as PauseOutlinedIcon, SelectOutlined as SelectOutlinedIcon, + StopOutlined as StopOutlinedIcon, SyncOutlined as SyncOutlinedIcon, } from "@ant-design/icons"; import { useRequest } from "ahooks"; @@ -75,7 +75,7 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => { ); } else if (record.status === WORKFLOW_RUN_STATUSES.CANCELED) { return ( - } color="warning"> + } color="warning"> {t("workflow_run.props.status.canceled")} ); diff --git a/ui/src/pages/dashboard/Dashboard.tsx b/ui/src/pages/dashboard/Dashboard.tsx index d80c8c10..b5c48cd2 100644 --- a/ui/src/pages/dashboard/Dashboard.tsx +++ b/ui/src/pages/dashboard/Dashboard.tsx @@ -7,10 +7,10 @@ import { ClockCircleOutlined as ClockCircleOutlinedIcon, CloseCircleOutlined as CloseCircleOutlinedIcon, LockOutlined as LockOutlinedIcon, - PauseCircleOutlined as PauseCircleOutlinedIcon, PlusOutlined as PlusOutlinedIcon, SelectOutlined as SelectOutlinedIcon, SendOutlined as SendOutlinedIcon, + StopOutlined as StopOutlinedIcon, SyncOutlined as SyncOutlinedIcon, } from "@ant-design/icons"; import { PageHeader } from "@ant-design/pro-components"; @@ -89,7 +89,6 @@ const Dashboard = () => { const workflow = record.expand?.workflowId; return ( { if (workflow) { @@ -129,7 +128,7 @@ const Dashboard = () => { ); } else if (record.status === WORKFLOW_RUN_STATUSES.CANCELED) { return ( - } color="warning"> + } color="warning"> {t("workflow_run.props.status.canceled")} ); diff --git a/ui/src/pages/workflows/WorkflowDetail.tsx b/ui/src/pages/workflows/WorkflowDetail.tsx index 4a2f8022..7db866e3 100644 --- a/ui/src/pages/workflows/WorkflowDetail.tsx +++ b/ui/src/pages/workflows/WorkflowDetail.tsx @@ -42,7 +42,6 @@ const WorkflowDetail = () => { useZustandShallowSelector(["workflow", "initialized", "init", "destroy", "setEnabled", "release", "discard"]) ); useEffect(() => { - // TODO: loading & error workflowState.init(workflowId!); return () => { @@ -52,7 +51,7 @@ const WorkflowDetail = () => { const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration"); - const [isRunning, setIsRunning] = useState(false); + const [isPendingOrRunning, setIsPendingOrRunning] = useState(false); const lastRunStatus = useMemo(() => workflow.lastRunStatus, [workflow]); const [allowDiscard, setAllowDiscard] = useState(false); @@ -60,14 +59,14 @@ const WorkflowDetail = () => { const [allowRun, setAllowRun] = useState(false); useEffect(() => { - setIsRunning(lastRunStatus == WORKFLOW_RUN_STATUSES.PENDING || lastRunStatus == WORKFLOW_RUN_STATUSES.RUNNING); + setIsPendingOrRunning(lastRunStatus == WORKFLOW_RUN_STATUSES.PENDING || lastRunStatus == WORKFLOW_RUN_STATUSES.RUNNING); }, [lastRunStatus]); useEffect(() => { - if (!!workflowId && isRunning) { + if (!!workflowId && isPendingOrRunning) { subscribeWorkflow(workflowId, (e) => { if (e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.PENDING && e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.RUNNING) { - setIsRunning(false); + setIsPendingOrRunning(false); unsubscribeWorkflow(workflowId); } }); @@ -76,15 +75,15 @@ const WorkflowDetail = () => { unsubscribeWorkflow(workflowId); }; } - }, [workflowId, isRunning]); + }, [workflowId, isPendingOrRunning]); useEffect(() => { const hasReleased = !!workflow.content; const hasChanges = workflow.hasDraft! || !isEqual(workflow.draft, workflow.content); - setAllowDiscard(!isRunning && hasReleased && hasChanges); - setAllowRelease(!isRunning && hasChanges); + setAllowDiscard(!isPendingOrRunning && hasReleased && hasChanges); + setAllowRelease(!isPendingOrRunning && hasChanges); setAllowRun(hasReleased); - }, [workflow.content, workflow.draft, workflow.hasDraft, isRunning]); + }, [workflow.content, workflow.draft, workflow.hasDraft, isPendingOrRunning]); const handleEnableChange = async () => { if (!workflow.enabled && (!workflow.content || !isAllNodesValidated(workflow.content))) { @@ -174,12 +173,12 @@ const WorkflowDetail = () => { let unsubscribeFn: Awaited> | undefined = undefined; try { - setIsRunning(true); + setIsPendingOrRunning(true); // subscribe before running workflow unsubscribeFn = await subscribeWorkflow(workflowId!, (e) => { if (e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.PENDING && e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.RUNNING) { - setIsRunning(false); + setIsPendingOrRunning(false); unsubscribeFn?.(); } }); @@ -188,7 +187,7 @@ const WorkflowDetail = () => { messageApi.info(t("workflow.detail.orchestration.action.run.prompt")); } catch (err) { - setIsRunning(false); + setIsPendingOrRunning(false); unsubscribeFn?.(); console.error(err); @@ -279,7 +278,7 @@ const WorkflowDetail = () => {
- diff --git a/ui/src/pages/workflows/WorkflowList.tsx b/ui/src/pages/workflows/WorkflowList.tsx index 29dad70d..c0241413 100644 --- a/ui/src/pages/workflows/WorkflowList.tsx +++ b/ui/src/pages/workflows/WorkflowList.tsx @@ -7,8 +7,8 @@ import { CloseCircleOutlined as CloseCircleOutlinedIcon, DeleteOutlined as DeleteOutlinedIcon, EditOutlined as EditOutlinedIcon, - PauseCircleOutlined as PauseCircleOutlinedIcon, PlusOutlined as PlusOutlinedIcon, + StopOutlined as StopOutlinedIcon, SyncOutlined as SyncOutlinedIcon, } from "@ant-design/icons"; @@ -170,7 +170,7 @@ const WorkflowList = () => { } else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.FAILED) { icon = ; } else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.CANCELED) { - icon = ; + icon = ; } return ( From a74ec95a6a5d657606ecc6ebf89763b8dc011706 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Sat, 8 Feb 2025 23:03:31 +0800 Subject: [PATCH 14/23] feat(ui): subscribe workflow runs status --- ui/src/components/workflow/WorkflowRuns.tsx | 30 +++++++++++++++++++-- ui/src/pages/workflows/WorkflowDetail.tsx | 4 +-- ui/src/repository/workflow.ts | 8 ++---- ui/src/repository/workflowRun.ts | 12 ++++++++- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/ui/src/components/workflow/WorkflowRuns.tsx b/ui/src/components/workflow/WorkflowRuns.tsx index e670e102..02bfb453 100644 --- a/ui/src/components/workflow/WorkflowRuns.tsx +++ b/ui/src/components/workflow/WorkflowRuns.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { CheckCircleOutlined as CheckCircleOutlinedIcon, @@ -18,7 +18,12 @@ import { ClientResponseError } from "pocketbase"; import { cancelRun as cancelWorkflowRun } from "@/api/workflows"; import { WORKFLOW_TRIGGERS } from "@/domain/workflow"; import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun"; -import { list as listWorkflowRuns, remove as removeWorkflowRun } from "@/repository/workflowRun"; +import { + list as listWorkflowRuns, + remove as removeWorkflowRun, + subscribe as subscribeWorkflowRun, + unsubscribe as unsubscribeWorkflowRun, +} from "@/repository/workflowRun"; import { getErrMsg } from "@/utils/error"; import WorkflowRunDetailDrawer from "./WorkflowRunDetailDrawer"; @@ -211,6 +216,27 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => { } ); + useEffect(() => { + const items = tableData.filter((e) => e.status === WORKFLOW_RUN_STATUSES.PENDING || e.status === WORKFLOW_RUN_STATUSES.RUNNING); + for (const item of items) { + subscribeWorkflowRun(item.id, (cb) => { + setTableData((prev) => { + const index = prev.findIndex((e) => e.id === item.id); + if (index !== -1) { + prev[index] = cb.record; + } + return [...prev]; + }); + }); + } + + return () => { + for (const item of items) { + unsubscribeWorkflowRun(item.id); + } + }; + }, [tableData]); + const handleCancelClick = (workflowRun: WorkflowRunModel) => { modalApi.confirm({ title: t("workflow_run.action.cancel"), diff --git a/ui/src/pages/workflows/WorkflowDetail.tsx b/ui/src/pages/workflows/WorkflowDetail.tsx index 7db866e3..df931504 100644 --- a/ui/src/pages/workflows/WorkflowDetail.tsx +++ b/ui/src/pages/workflows/WorkflowDetail.tsx @@ -64,8 +64,8 @@ const WorkflowDetail = () => { useEffect(() => { if (!!workflowId && isPendingOrRunning) { - subscribeWorkflow(workflowId, (e) => { - if (e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.PENDING && e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.RUNNING) { + subscribeWorkflow(workflowId, (cb) => { + if (cb.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.PENDING && cb.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.RUNNING) { setIsPendingOrRunning(false); unsubscribeWorkflow(workflowId); } diff --git a/ui/src/repository/workflow.ts b/ui/src/repository/workflow.ts index 54ddddd4..0241eecf 100644 --- a/ui/src/repository/workflow.ts +++ b/ui/src/repository/workflow.ts @@ -50,13 +50,9 @@ export const remove = async (record: MaybeModelRecordWithId) => { }; export const subscribe = async (id: string, cb: (e: RecordSubscription) => void) => { - const pb = getPocketBase(); - - return pb.collection("workflow").subscribe(id, cb); + return getPocketBase().collection(COLLECTION_NAME).subscribe(id, cb); }; export const unsubscribe = async (id: string) => { - const pb = getPocketBase(); - - return pb.collection("workflow").unsubscribe(id); + return getPocketBase().collection(COLLECTION_NAME).unsubscribe(id); }; diff --git a/ui/src/repository/workflowRun.ts b/ui/src/repository/workflowRun.ts index 0aa88080..cf681b70 100644 --- a/ui/src/repository/workflowRun.ts +++ b/ui/src/repository/workflowRun.ts @@ -1,4 +1,6 @@ -import { type WorkflowRunModel } from "@/domain/workflowRun"; +import { type RecordSubscription } from "pocketbase"; + +import { type WorkflowRunModel } from "@/domain/workflowRun"; import { getPocketBase } from "./_pocketbase"; @@ -35,3 +37,11 @@ export const list = async (request: ListWorkflowRunsRequest) => { 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) => { + return getPocketBase().collection(COLLECTION_NAME).subscribe(id, cb); +}; + +export const unsubscribe = async (id: string) => { + return getPocketBase().collection(COLLECTION_NAME).unsubscribe(id); +}; From b8513eb0b688d54e19ea54d3f40c3e19795078b7 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 10 Feb 2025 09:59:03 +0800 Subject: [PATCH 15/23] fix: different cronexpr rules between ui and pocketbase --- internal/repository/workflow.go | 2 +- internal/scheduler/certificate.go | 2 +- internal/scheduler/scheduler.go | 9 ++++++-- internal/scheduler/workflow.go | 2 +- internal/workflow/dispatcher/dispatcher.go | 6 +++++ internal/workflow/event.go | 1 - internal/workflow/service.go | 8 ++++++- .../i18n/locales/en/nls.workflow.nodes.json | 2 +- .../i18n/locales/zh/nls.workflow.nodes.json | 2 +- ui/src/utils/cron.ts | 22 +++++++++---------- 10 files changed, 35 insertions(+), 21 deletions(-) diff --git a/internal/repository/workflow.go b/internal/repository/workflow.go index 60d60899..baa5e21b 100644 --- a/internal/repository/workflow.go +++ b/internal/repository/workflow.go @@ -24,7 +24,7 @@ func (r *WorkflowRepository) ListEnabledAuto(ctx context.Context) ([]*domain.Wor "enabled={:enabled} && trigger={:trigger}", "-created", 0, 0, - dbx.Params{"enabled": true, "trigger": domain.WorkflowTriggerTypeAuto}, + dbx.Params{"enabled": true, "trigger": string(domain.WorkflowTriggerTypeAuto)}, ) if err != nil { return nil, err diff --git a/internal/scheduler/certificate.go b/internal/scheduler/certificate.go index 26c7311f..43887cb0 100644 --- a/internal/scheduler/certificate.go +++ b/internal/scheduler/certificate.go @@ -6,6 +6,6 @@ type certificateService interface { InitSchedule(ctx context.Context) error } -func NewCertificateScheduler(service certificateService) error { +func InitCertificateScheduler(service certificateService) error { return service.InitSchedule(context.Background()) } diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go index f5029599..91dbf115 100644 --- a/internal/scheduler/scheduler.go +++ b/internal/scheduler/scheduler.go @@ -1,6 +1,7 @@ package scheduler import ( + "github.com/usual2970/certimate/internal/app" "github.com/usual2970/certimate/internal/certificate" "github.com/usual2970/certimate/internal/repository" "github.com/usual2970/certimate/internal/workflow" @@ -14,7 +15,11 @@ func Register() { certificateRepo := repository.NewCertificateRepository() certificateSvc := certificate.NewCertificateService(certificateRepo) - NewCertificateScheduler(certificateSvc) + if err := InitWorkflowScheduler(workflowSvc); err != nil { + app.GetLogger().Error("failed to init workflow scheduler", "err", err) + } - NewWorkflowScheduler(workflowSvc) + if err := InitCertificateScheduler(certificateSvc); err != nil { + app.GetLogger().Error("failed to init certificate scheduler", "err", err) + } } diff --git a/internal/scheduler/workflow.go b/internal/scheduler/workflow.go index 7cb4dfa8..cef4adce 100644 --- a/internal/scheduler/workflow.go +++ b/internal/scheduler/workflow.go @@ -6,6 +6,6 @@ type workflowService interface { InitSchedule(ctx context.Context) error } -func NewWorkflowScheduler(service workflowService) error { +func InitWorkflowScheduler(service workflowService) error { return service.InitSchedule(context.Background()) } diff --git a/internal/workflow/dispatcher/dispatcher.go b/internal/workflow/dispatcher/dispatcher.go index 0ecc0828..0b504c25 100644 --- a/internal/workflow/dispatcher/dispatcher.go +++ b/internal/workflow/dispatcher/dispatcher.go @@ -92,6 +92,7 @@ func (w *WorkflowDispatcher) Dispatch(data *WorkflowWorkerData) { } w.enqueueWorker(data) + select { case w.chWork <- data: default: @@ -138,6 +139,11 @@ func (w *WorkflowDispatcher) Shutdown() { w.queueMutex.Unlock() // 等待所有正在执行的 WorkflowRun 完成 + w.workerMutex.Lock() + for _, worker := range w.workers { + worker.Cancel() + } + w.workerMutex.Unlock() w.wg.Wait() w.workers = make(map[string]*workflowWorker) w.workerIdMap = make(map[string]string) diff --git a/internal/workflow/event.go b/internal/workflow/event.go index fa6b4b1a..0fedd67b 100644 --- a/internal/workflow/event.go +++ b/internal/workflow/event.go @@ -72,7 +72,6 @@ func onWorkflowRecordCreateOrUpdate(ctx context.Context, record *core.Record) er }) }) if err != nil { - app.GetLogger().Error("add cron job failed", "err", err) return fmt.Errorf("add cron job failed: %w", err) } diff --git a/internal/workflow/service.go b/internal/workflow/service.go index 2d0a224d..d2236a8d 100644 --- a/internal/workflow/service.go +++ b/internal/workflow/service.go @@ -48,6 +48,8 @@ func (s *WorkflowService) InitSchedule(ctx context.Context) error { scheduler := app.GetScheduler() for _, workflow := range workflows { + var errs []error + err := scheduler.Add(fmt.Sprintf("workflow#%s", workflow.Id), workflow.TriggerCron, func() { s.StartRun(ctx, &dtos.WorkflowStartRunReq{ WorkflowId: workflow.Id, @@ -55,7 +57,11 @@ func (s *WorkflowService) InitSchedule(ctx context.Context) error { }) }) if err != nil { - return err + errs = append(errs, err) + } + + if len(errs) > 0 { + return errors.Join(errs...) } } diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 964633e0..84f0f48e 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -18,7 +18,7 @@ "workflow_node.start.form.trigger_cron.label": "Cron expression", "workflow_node.start.form.trigger_cron.placeholder": "Please enter cron expression", "workflow_node.start.form.trigger_cron.errmsg.invalid": "Please enter a valid cron expression", - "workflow_node.start.form.trigger_cron.tooltip": "Time zone is based on the server.", + "workflow_node.start.form.trigger_cron.tooltip": "Exactly 5 space separated segments. Time zone is based on the server.", "workflow_node.start.form.trigger_cron.extra": "Expected execution time for the last 5 times:", "workflow_node.start.form.trigger_cron.guide": "Tips: If you have multiple workflows, it is recommended to set them to run at multiple times of the day instead of always running at specific times.

Reference links:
1. Let’s Encrypt rate limits
2. Why should my Let’s Encrypt (ACME) client run at a random time?", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index d132f935..40860e1e 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -18,7 +18,7 @@ "workflow_node.start.form.trigger_cron.label": "Cron 表达式", "workflow_node.start.form.trigger_cron.placeholder": "请输入 Cron 表达式", "workflow_node.start.form.trigger_cron.errmsg.invalid": "请输入正确的 Cron 表达式", - "workflow_node.start.form.trigger_cron.tooltip": "支持使用任意值(即 *)、值列表分隔符(即 ,)、值的范围(即 -)、步骤值(即 /)等四种表达式,时区以服务器设置为准。", + "workflow_node.start.form.trigger_cron.tooltip": "五段式表达式,支持使用任意值(即 *)、值列表分隔符(即 ,)、值的范围(即 -)、步骤值(即 /)等四种表达式。时区以服务器设置为准。", "workflow_node.start.form.trigger_cron.extra": "预计最近 5 次执行时间:", "workflow_node.start.form.trigger_cron.guide": "小贴士:如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。

参考链接:
1. Let’s Encrypt 速率限制
2. 为什么我的 Let’s Encrypt (ACME) 客户端启动时间应当随机?", diff --git a/ui/src/utils/cron.ts b/ui/src/utils/cron.ts index c2ca22e2..79a94ddc 100644 --- a/ui/src/utils/cron.ts +++ b/ui/src/utils/cron.ts @@ -3,6 +3,8 @@ export const validCronExpression = (expr: string): boolean => { try { parseExpression(expr); + + if (expr.trim().split(" ").length !== 5) return false; // pocketbase 后端仅支持五段式的表达式 return true; } catch { return false; @@ -10,19 +12,15 @@ export const validCronExpression = (expr: string): boolean => { }; export const getNextCronExecutions = (expr: string, times = 1): Date[] => { - if (!expr) return []; + if (!validCronExpression(expr)) return []; - try { - const now = new Date(); - const cron = parseExpression(expr, { currentDate: now, iterator: true }); + const now = new Date(); + const cron = parseExpression(expr, { currentDate: now, iterator: true }); - const result: Date[] = []; - for (let i = 0; i < times; i++) { - const next = cron.next(); - result.push(next.value.toDate()); - } - return result; - } catch { - return []; + const result: Date[] = []; + for (let i = 0; i < times; i++) { + const next = cron.next(); + result.push(next.value.toDate()); } + return result; }; From 75c89b3d0be4f179d641b04d87a1422590f3d482 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 10 Feb 2025 12:13:54 +0800 Subject: [PATCH 16/23] feat(ui): display artifact certificates in WorkflowRunDetail --- internal/repository/workflow_output.go | 19 ++- .../components/workflow/WorkflowRunDetail.tsx | 110 +++++++++++++++++- ui/src/components/workflow/WorkflowRuns.tsx | 2 +- .../workflow/node/NotifyNodeConfigForm.tsx | 2 +- .../components/workflow/node/UploadNode.tsx | 3 +- .../workflow/node/UploadNodeConfigForm.tsx | 4 +- ui/src/domain/workflowRun.ts | 2 +- ui/src/i18n/locales/en/nls.workflow.runs.json | 8 +- ui/src/i18n/locales/zh/nls.workflow.runs.json | 8 +- ui/src/pages/accesses/AccessList.tsx | 2 +- ui/src/pages/certificates/CertificateList.tsx | 2 +- ui/src/pages/dashboard/Dashboard.tsx | 8 +- ui/src/pages/workflows/WorkflowList.tsx | 2 +- ui/src/repository/certificate.ts | 17 +++ 14 files changed, 170 insertions(+), 19 deletions(-) diff --git a/internal/repository/workflow_output.go b/internal/repository/workflow_output.go index e75b2cb7..4cee625c 100644 --- a/internal/repository/workflow_output.go +++ b/internal/repository/workflow_output.go @@ -61,7 +61,22 @@ func (r *WorkflowOutputRepository) SaveWithCertificate(ctx context.Context, work workflowOutput.UpdatedAt = record.GetDateTime("updated").Time() } - if certificate != nil { + if certificate == nil { + panic("certificate is nil") + } else { + if certificate.WorkflowId != "" && certificate.WorkflowId != workflowOutput.WorkflowId { + return workflowOutput, fmt.Errorf("certificate #%s is not belong to workflow #%s", certificate.Id, workflowOutput.WorkflowId) + } + if certificate.WorkflowRunId != "" && certificate.WorkflowRunId != workflowOutput.RunId { + return workflowOutput, fmt.Errorf("certificate #%s is not belong to workflow run #%s", certificate.Id, workflowOutput.RunId) + } + if certificate.WorkflowNodeId != "" && certificate.WorkflowNodeId != workflowOutput.NodeId { + return workflowOutput, fmt.Errorf("certificate #%s is not belong to workflow node #%s", certificate.Id, workflowOutput.NodeId) + } + if certificate.WorkflowOutputId != "" && certificate.WorkflowOutputId != workflowOutput.Id { + return workflowOutput, fmt.Errorf("certificate #%s is not belong to workflow output #%s", certificate.Id, workflowOutput.Id) + } + certificate.WorkflowId = workflowOutput.WorkflowId certificate.WorkflowRunId = workflowOutput.RunId certificate.WorkflowNodeId = workflowOutput.NodeId @@ -143,5 +158,5 @@ func (r *WorkflowOutputRepository) saveRecord(workflowOutput *domain.WorkflowOut return record, err } - return record, err + return record, nil } diff --git a/ui/src/components/workflow/WorkflowRunDetail.tsx b/ui/src/components/workflow/WorkflowRunDetail.tsx index db9610f7..d2a20a5f 100644 --- a/ui/src/components/workflow/WorkflowRunDetail.tsx +++ b/ui/src/components/workflow/WorkflowRunDetail.tsx @@ -1,9 +1,17 @@ +import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { Alert, Typography } from "antd"; +import { SelectOutlined as SelectOutlinedIcon } from "@ant-design/icons"; +import { useRequest } from "ahooks"; +import { Alert, Button, Divider, Empty, Table, type TableProps, Tooltip, Typography, notification } from "antd"; import dayjs from "dayjs"; +import { ClientResponseError } from "pocketbase"; +import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer"; import Show from "@/components/Show"; +import { type CertificateModel } from "@/domain/certificate"; import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun"; +import { listByWorkflowRunId as listCertificateByWorkflowRunId } from "@/repository/certificate"; +import { getErrMsg } from "@/utils/error"; export type WorkflowRunDetailProps = { className?: string; @@ -45,8 +53,108 @@ const WorkflowRunDetail = ({ data, ...props }: WorkflowRunDetailProps) => { })}
+ + + + + + ); }; +const WorkflowRunArtifacts = ({ runId }: { runId: string }) => { + const { t } = useTranslation(); + + const [notificationApi, NotificationContextHolder] = notification.useNotification(); + + const tableColumns: TableProps["columns"] = [ + { + key: "$index", + align: "center", + fixed: "left", + width: 50, + render: (_, __, index) => index + 1, + }, + { + key: "type", + title: t("workflow_run_artifact.props.type"), + render: () => t("workflow_run_artifact.props.type.certificate"), + }, + { + key: "name", + title: t("workflow_run_artifact.props.name"), + ellipsis: true, + render: (_, record) => { + return ( + + {record.subjectAltNames} + + ); + }, + }, + { + key: "$action", + align: "end", + width: 120, + render: (_, record) => ( + + +