diff --git a/go.mod b/go.mod index b91c2e36..1567878b 100644 --- a/go.mod +++ b/go.mod @@ -18,14 +18,14 @@ require ( github.com/baidubce/bce-sdk-go v0.9.214 github.com/byteplus-sdk/byteplus-sdk-golang v1.0.40 github.com/go-acme/lego/v4 v4.21.0 - github.com/go-resty/resty/v2 v2.16.3 + github.com/go-resty/resty/v2 v2.16.4 github.com/go-viper/mapstructure/v2 v2.2.1 github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.132 github.com/nikoksr/notify v1.3.0 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.3 + github.com/pocketbase/pocketbase v0.24.4 github.com/qiniu/go-sdk/v7 v7.25.2 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1084 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1084 @@ -73,7 +73,6 @@ require ( github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect @@ -92,9 +91,6 @@ require ( k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect - modernc.org/gc/v3 v3.0.0-20250105121824-520be1a3aee6 // indirect - modernc.org/strutil v1.2.1 // indirect - modernc.org/token v1.1.0 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect @@ -115,22 +111,22 @@ require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go-v2 v1.33.0 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect - github.com/aws/aws-sdk-go-v2/config v1.29.0 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.53 // indirect + github.com/aws/aws-sdk-go-v2/config v1.29.1 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.54 // indirect 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.51 // 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.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.1 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.24.10 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.9 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.33.8 // 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/cenkalti/backoff/v4 v4.3.0 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect @@ -192,8 +188,8 @@ require ( google.golang.org/protobuf v1.36.3 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.55.3 // indirect + modernc.org/libc v1.61.9 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.8.2 // indirect - modernc.org/sqlite v1.34.4 // indirect + modernc.org/sqlite v1.34.5 // indirect ) diff --git a/go.sum b/go.sum index 27e5c075..bbb65a6a 100644 --- a/go.sum +++ b/go.sum @@ -214,14 +214,14 @@ github.com/aws/aws-sdk-go-v2 v1.33.0 h1:Evgm4DI9imD81V0WwD+TN4DCwjUMdc94TrduMLbg 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.0 h1:Vk/u4jof33or1qAQLdofpjKV7mQQT7DcUpnYx8kdmxY= -github.com/aws/aws-sdk-go-v2/config v1.29.0/go.mod h1:iXAZK3Gxvpq3tA+B9WaDYpZis7M8KFgdrDPMmHrgbJM= -github.com/aws/aws-sdk-go-v2/credentials v1.17.53 h1:lwrVhiEDW5yXsuVKlFVUnR2R50zt2DklhOyeLETqDuE= -github.com/aws/aws-sdk-go-v2/credentials v1.17.53/go.mod h1:CkqM1bIw/xjEpBMhBnvqUXYZbpCFuj6dnCAyDk2AtAY= +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.51 h1:Q0FNHs6JTGuoBWNQycD5LRSf+/WVHWEl+FwJ0tEDZUE= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.51/go.mod h1:B9sW5/AD5bStKdTyUdz1xWRKOwnyUwJ4eJ4olQBtZo0= +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= @@ -233,22 +233,22 @@ github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.28/go.mod h1:pyaOYEdp1MJWgtXLy6q8 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.1 h1:mJ9FRktB8v1Ihpqwfk0AWvYEd0FgQtLsshc2Qb2TVc8= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.1/go.mod h1:dIW8puxSbYLSPv/ju0d9A3CpwXdtqvJtYKDMVmPLOWE= +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/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.1 h1:OzmyfYGiMCOIAq5pa0KWcaZoA9F8FqajOJevh+hhFdY= -github.com/aws/aws-sdk-go-v2/service/s3 v1.73.1/go.mod h1:K+0a0kWDHAUXBH8GvYGS3cQRwIuRjO9bMWUz6vpNCaU= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.10 h1:DyZUj3xSw3FR3TXSwDhPhuZkkT14QHBiacdbUVcD0Dg= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.10/go.mod h1:Ro744S4fKiCCuZECXgOi760TiYylUM8ZBf6OGiZzJtY= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.9 h1:I1TsPEs34vbpOnR81GIcAq4/3Ud+jRHVGwx6qLQUHLs= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.9/go.mod h1:Fzsj6lZEb8AkTE5S68OhcbBqeWPsR8RnGuKPr8Todl8= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.8 h1:pqEJQtlKWvnv3B6VRt60ZmsHy3SotlEBvfUBPB1KVcM= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.8/go.mod h1:f6vjfZER1M17Fokn0IzssOTMT2N8ZSq+7jnNF0tArvw= +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/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= @@ -393,8 +393,8 @@ github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/validator/v10 v10.7.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk= -github.com/go-resty/resty/v2 v2.16.3 h1:zacNT7lt4b8M/io2Ahj6yPypL7bqx9n1iprfQuodV+E= -github.com/go-resty/resty/v2 v2.16.3/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= +github.com/go-resty/resty/v2 v2.16.4 h1:81IjtszQKwbz7dot4LLYGwhJNUsNwECD2O7nru5q60E= +github.com/go-resty/resty/v2 v2.16.4/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= @@ -542,8 +542,6 @@ github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= @@ -723,8 +721,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.3 h1:WUrzW11ijCySlDsVRHon3HXdtiratWv+ODK26/k6cI8= -github.com/pocketbase/pocketbase v0.24.3/go.mod h1:EfXV/8RUY76jA6g1RPNHjOuW7wTd2bz0QlvAI/RU8YY= +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/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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -1385,29 +1383,27 @@ k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8X k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7/go.mod h1:GewRfANuJ70iYzvn+i4lezLDAFzvjxZYK1gn1lWcfas= k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= -modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= -modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= -modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= +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/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.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= -modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= -modernc.org/gc/v3 v3.0.0-20250105121824-520be1a3aee6 h1:JoKwHjIFumiKrjMbp1cNbC5E9UyCgA/ZcID0xOWQ2N8= -modernc.org/gc/v3 v3.0.0-20250105121824-520be1a3aee6/go.mod h1:LG5UO1Ran4OO0JRKz2oNiXhR5nNrgz0PzH7UKhz0aMU= -modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= -modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= +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/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= modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= -modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= -modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= -modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= -modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8= -modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g= +modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/internal/applicant/acme_user.go b/internal/applicant/acme_user.go index daa7a4cf..1ab4c424 100644 --- a/internal/applicant/acme_user.go +++ b/internal/applicant/acme_user.go @@ -82,9 +82,9 @@ type acmeAccountRepository interface { var registerGroup singleflight.Group -func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) { +func registerAcmeUserWithSingleFlight(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) { resp, err, _ := registerGroup.Do(fmt.Sprintf("register_acme_user_%s_%s", sslProviderConfig.Provider, user.GetEmail()), func() (interface{}, error) { - return register(client, sslProviderConfig, user) + return registerAcmeUser(client, sslProviderConfig, user) }) if err != nil { @@ -94,7 +94,7 @@ func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderCon return resp.(*registration.Resource), nil } -func register(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) { +func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) { var reg *registration.Resource var err error switch sslProviderConfig.Provider { @@ -123,7 +123,6 @@ func register(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, use } repo := repository.NewAcmeAccountRepository() - resp, err := repo.GetByCAAndEmail(sslProviderConfig.Provider, user.GetEmail()) if err == nil { user.privkey = resp.Key diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go index abee203a..a612ebda 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -159,7 +159,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap // New users need to register first if !acmeUser.hasRegistration() { - reg, err := registerAcmeUser(client, sslProviderConfig, acmeUser) + reg, err := registerAcmeUserWithSingleFlight(client, sslProviderConfig, acmeUser) if err != nil { return nil, fmt.Errorf("failed to register: %w", err) } diff --git a/internal/certificate/service.go b/internal/certificate/service.go index ac08f449..b8f5fa89 100644 --- a/internal/certificate/service.go +++ b/internal/certificate/service.go @@ -25,8 +25,8 @@ const ( ) type certificateRepository interface { - GetById(ctx context.Context, id string) (*domain.Certificate, error) ListExpireSoon(ctx context.Context) ([]*domain.Certificate, error) + GetById(ctx context.Context, id string) (*domain.Certificate, error) } type CertificateService struct { diff --git a/internal/domain/dtos/certificate.go b/internal/domain/dtos/certificate.go index 8c9e80ae..cf9eb785 100644 --- a/internal/domain/dtos/certificate.go +++ b/internal/domain/dtos/certificate.go @@ -5,11 +5,6 @@ type CertificateArchiveFileReq struct { Format string `json:"format"` } -type CertificateArchiveFileResp struct { - Certificate string `json:"certificate"` - PrivateKey string `json:"privateKey"` -} - type CertificateValidateCertificateReq struct { Certificate string `json:"certificate"` } diff --git a/internal/domain/dtos/workflow.go b/internal/domain/dtos/workflow.go index 3a760971..9d1f5781 100644 --- a/internal/domain/dtos/workflow.go +++ b/internal/domain/dtos/workflow.go @@ -2,7 +2,12 @@ import "github.com/usual2970/certimate/internal/domain" -type WorkflowRunReq struct { +type WorkflowStartRunReq struct { WorkflowId string `json:"-"` Trigger domain.WorkflowTriggerType `json:"trigger"` } + +type WorkflowCancelRunReq struct { + WorkflowId string `json:"-"` + RunId string `json:"-"` +} diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index 7803b920..ac8fbce6 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -69,11 +69,11 @@ type WorkflowNodeConfigForApply struct { ProviderConfig map[string]any `json:"providerConfig"` // DNS 提供商额外配置 KeyAlgorithm string `json:"keyAlgorithm"` // 密钥算法 Nameservers string `json:"nameservers"` // DNS 服务器列表,以半角逗号分隔 - DnsPropagationTimeout int32 `json:"dnsPropagationTimeout"` // DNS 传播超时时间(默认取决于提供商) - DnsTTL int32 `json:"dnsTTL"` // DNS TTL(默认取决于提供商) - DisableFollowCNAME bool `json:"disableFollowCNAME"` // 是否禁用 CNAME 跟随 - DisableARI bool `json:"disableARI"` // 是否禁用 ARI - SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays"` // 证书到期前多少天前跳过续期(默认值:30) + DnsPropagationTimeout int32 `json:"dnsPropagationTimeout"` // DNS 传播超时时间(零值取决于提供商的默认值) + DnsTTL int32 `json:"dnsTTL"` // DNS TTL(零值取决于提供商的默认值) + DisableFollowCNAME bool `json:"disableFollowCNAME"` // 是否关闭 CNAME 跟随 + DisableARI bool `json:"disableARI"` // 是否关闭 ARI + SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays"` // 证书到期前多少天前跳过续期(零值将使用默认值 30) } type WorkflowNodeConfigForUpload struct { diff --git a/internal/domain/workflow_run.go b/internal/domain/workflow_run.go index 27ec30de..25ba1d7a 100644 --- a/internal/domain/workflow_run.go +++ b/internal/domain/workflow_run.go @@ -1,6 +1,9 @@ package domain -import "time" +import ( + "strings" + "time" +) const CollectionNameWorkflowRun = "workflow_run" @@ -22,6 +25,7 @@ const ( WorkflowRunStatusTypeRunning WorkflowRunStatusType = "running" WorkflowRunStatusTypeSucceeded WorkflowRunStatusType = "succeeded" WorkflowRunStatusTypeFailed WorkflowRunStatusType = "failed" + WorkflowRunStatusTypeCanceled WorkflowRunStatusType = "canceled" ) type WorkflowRunLog struct { @@ -40,12 +44,13 @@ type WorkflowRunLogOutput struct { type WorkflowRunLogs []WorkflowRunLog -func (r WorkflowRunLogs) FirstError() string { +func (r WorkflowRunLogs) ErrorString() string { + var builder strings.Builder for _, log := range r { if log.Error != "" { - return log.Error + builder.WriteString(log.Error) + builder.WriteString("\n") } } - - return "" + return builder.String() } diff --git a/internal/pkg/core/logger/builtin.go b/internal/pkg/core/logger/builtin.go index d3209d70..9787817d 100644 --- a/internal/pkg/core/logger/builtin.go +++ b/internal/pkg/core/logger/builtin.go @@ -5,6 +5,8 @@ import ( "fmt" "reflect" "strings" + + "github.com/usual2970/certimate/internal/pkg/utils/types" ) // 表示默认的日志记录器类型。 @@ -21,7 +23,7 @@ func (l *DefaultLogger) Logt(tag string, data ...any) { temp[0] = tag for i, v := range data { s := "" - if v == nil { + if types.IsNil(v) { s = "" } else { switch reflect.ValueOf(v).Kind() { diff --git a/internal/pkg/utils/certs/parser.go b/internal/pkg/utils/certs/parser.go index d03d6395..89338336 100644 --- a/internal/pkg/utils/certs/parser.go +++ b/internal/pkg/utils/certs/parser.go @@ -1,12 +1,14 @@ package certs import ( + "crypto" "crypto/ecdsa" "crypto/rsa" "crypto/x509" "encoding/pem" "errors" + "github.com/go-acme/lego/v4/certcrypto" xerrors "github.com/pkg/errors" ) @@ -34,6 +36,19 @@ func ParseCertificateFromPEM(certPem string) (cert *x509.Certificate, err error) return cert, nil } +// 从 PEM 编码的私钥字符串解析并返回一个 crypto.PrivateKey 对象。 +// +// 入参: +// - privkeyPem: 私钥 PEM 内容。 +// +// 出参: +// - privkey: crypto.PrivateKey 对象,可能是 rsa.PrivateKey、ecdsa.PrivateKey 或 ed25519.PrivateKey。 +// - err: 错误。 +func ParsePrivateKeyFromPEM(privkeyPem string) (privkey crypto.PrivateKey, err error) { + pemData := []byte(privkeyPem) + return certcrypto.ParsePEMPrivateKey(pemData) +} + // 从 PEM 编码的私钥字符串解析并返回一个 ecdsa.PrivateKey 对象。 // // 入参: diff --git a/internal/pkg/utils/certs/transformer.go b/internal/pkg/utils/certs/transformer.go index c3575b3e..60105d3e 100644 --- a/internal/pkg/utils/certs/transformer.go +++ b/internal/pkg/utils/certs/transformer.go @@ -2,8 +2,6 @@ import ( "bytes" - "crypto/ecdsa" - "crypto/rsa" "encoding/pem" "errors" "time" @@ -28,23 +26,9 @@ func TransformCertificateFromPEMToPFX(certPem string, privkeyPem string, pfxPass return nil, err } - var privkey interface{} - switch cert.PublicKey.(type) { - case *rsa.PublicKey: - { - privkey, err = ParsePKCS1PrivateKeyFromPEM(privkeyPem) - if err != nil { - return nil, err - } - } - - case *ecdsa.PublicKey: - { - privkey, err = ParseECPrivateKeyFromPEM(privkeyPem) - if err != nil { - return nil, err - } - } + privkey, err := ParsePrivateKeyFromPEM(privkeyPem) + if err != nil { + return nil, err } pfxData, err := pkcs12.LegacyRC2.Encode(privkey, cert, nil, pfxPassword) diff --git a/internal/pkg/utils/types/types.go b/internal/pkg/utils/types/types.go index b88467b1..cd2b1602 100644 --- a/internal/pkg/utils/types/types.go +++ b/internal/pkg/utils/types/types.go @@ -3,6 +3,7 @@ import "reflect" // 判断对象是否为 nil。 +// 与直接使用 `obj == nil` 不同,该函数会正确判断接口类型对象的真实值是否为空。 // // 入参: // - value:待判断的对象。 diff --git a/internal/repository/certificate.go b/internal/repository/certificate.go index 6aee6add..5e1a7f8d 100644 --- a/internal/repository/certificate.go +++ b/internal/repository/certificate.go @@ -19,11 +19,11 @@ func NewCertificateRepository() *CertificateRepository { } func (r *CertificateRepository) ListExpireSoon(ctx context.Context) ([]*domain.Certificate, error) { - records, err := app.GetApp().FindRecordsByFilter( + records, err := app.GetApp().FindAllRecords( domain.CollectionNameCertificate, - "expireAt>DATETIME('now') && expireAtDATETIME('now')"), + dbx.NewExp("expireAt 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 index ec6a8da4..47663136 100644 --- a/internal/workflow/processor/processor.go +++ b/internal/workflow/processor/processor.go @@ -19,15 +19,15 @@ func NewWorkflowProcessor(workflow *domain.Workflow) *workflowProcessor { } } -func (w *workflowProcessor) Log(ctx context.Context) []domain.WorkflowRunLog { - return w.logs -} - func (w *workflowProcessor) Run(ctx context.Context) error { ctx = setContextWorkflowId(ctx, w.workflow.Id) return w.processNode(ctx, w.workflow.Content) } +func (w *workflowProcessor) GetRunLogs() []domain.WorkflowRunLog { + return w.logs +} + func (w *workflowProcessor) processNode(ctx context.Context, node *domain.WorkflowNode) error { current := node for current != nil { @@ -39,8 +39,8 @@ func (w *workflowProcessor) processNode(ctx context.Context, node *domain.Workfl } } - var runErr error var processor nodes.NodeProcessor + var runErr error for { if current.Type != domain.WorkflowNodeTypeBranch && current.Type != domain.WorkflowNodeTypeExecuteResultBranch { processor, runErr = nodes.GetProcessor(current) @@ -49,7 +49,6 @@ func (w *workflowProcessor) processNode(ctx context.Context, node *domain.Workfl } runErr = processor.Run(ctx) - log := processor.Log(ctx) if log != nil { w.logs = append(w.logs, *log) @@ -58,6 +57,7 @@ func (w *workflowProcessor) processNode(ctx context.Context, node *domain.Workfl break } } + break } @@ -70,8 +70,8 @@ func (w *workflowProcessor) processNode(ctx context.Context, node *domain.Workfl } else { current = current.Next } - } + return nil } @@ -79,10 +79,6 @@ func setContextWorkflowId(ctx context.Context, id string) context.Context { return context.WithValue(ctx, "workflow_id", id) } -func GetWorkflowId(ctx context.Context) string { - return ctx.Value("workflow_id").(string) -} - 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 62f8a888..0a5f2f96 100644 --- a/internal/workflow/service.go +++ b/internal/workflow/service.go @@ -23,8 +23,8 @@ type workflowRunData struct { 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) error - SaveRun(ctx context.Context, workflowRun *domain.WorkflowRun) error + Save(ctx context.Context, workflow *domain.Workflow) (*domain.Workflow, error) + SaveRun(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error) } type WorkflowService struct { @@ -35,35 +35,20 @@ type WorkflowService struct { } func NewWorkflowService(repo workflowRepository) *WorkflowService { - rs := &WorkflowService{ + srv := &WorkflowService{ repo: repo, ch: make(chan *workflowRunData, 1), } ctx, cancel := context.WithCancel(context.Background()) - rs.cancel = cancel + srv.cancel = cancel - rs.wg.Add(defaultRoutines) + srv.wg.Add(defaultRoutines) for i := 0; i < defaultRoutines; i++ { - go rs.process(ctx) + go srv.run(ctx) } - return rs -} - -func (s *WorkflowService) process(ctx context.Context) { - defer s.wg.Done() - for { - select { - case data := <-s.ch: - // 执行 - if err := s.run(ctx, data); err != nil { - app.GetLogger().Error("failed to run workflow", "id", data.Workflow.Id, "err", err) - } - case <-ctx.Done(): - return - } - } + return srv } func (s *WorkflowService) InitSchedule(ctx context.Context) error { @@ -75,7 +60,7 @@ func (s *WorkflowService) InitSchedule(ctx context.Context) error { scheduler := app.GetScheduler() for _, workflow := range workflows { err := scheduler.Add(fmt.Sprintf("workflow#%s", workflow.Id), workflow.TriggerCron, func() { - s.Run(ctx, &dtos.WorkflowRunReq{ + s.StartRun(ctx, &dtos.WorkflowStartRunReq{ WorkflowId: workflow.Id, Trigger: domain.WorkflowTriggerTypeAuto, }) @@ -89,8 +74,7 @@ func (s *WorkflowService) InitSchedule(ctx context.Context) error { return nil } -func (s *WorkflowService) Run(ctx context.Context, req *dtos.WorkflowRunReq) error { - // 查询 +func (s *WorkflowService) StartRun(ctx context.Context, req *dtos.WorkflowStartRunReq) error { workflow, err := s.repo.GetById(ctx, req.WorkflowId) if err != nil { app.GetLogger().Error("failed to get workflow", "id", req.WorkflowId, "err", err) @@ -101,13 +85,13 @@ func (s *WorkflowService) Run(ctx context.Context, req *dtos.WorkflowRunReq) err return errors.New("workflow is running") } - // set last run workflow.LastRunTime = time.Now() - workflow.LastRunStatus = domain.WorkflowRunStatusTypeRunning + workflow.LastRunStatus = domain.WorkflowRunStatusTypePending workflow.LastRunId = "" - - if err := s.repo.Save(ctx, workflow); err != nil { + if resp, err := s.repo.Save(ctx, workflow); err != nil { return err + } else { + workflow = resp } s.ch <- &workflowRunData{ @@ -118,51 +102,70 @@ func (s *WorkflowService) Run(ctx context.Context, req *dtos.WorkflowRunReq) err return nil } -func (s *WorkflowService) run(ctx context.Context, runData *workflowRunData) error { - // 执行 - workflow := runData.Workflow - run := &domain.WorkflowRun{ - WorkflowId: workflow.Id, - Status: domain.WorkflowRunStatusTypeRunning, - Trigger: runData.RunTrigger, - StartedAt: time.Now(), - EndedAt: time.Now(), - } +func (s *WorkflowService) CancelRun(ctx context.Context, req *dtos.WorkflowCancelRunReq) error { + // TODO: 取消运行,防止因为某些原因意外挂起(如进程被杀死)导致工作流一直处于 running 状态无法重新运行 - processor := processor.NewWorkflowProcessor(workflow) - if err := processor.Run(ctx); err != nil { - run.Status = domain.WorkflowRunStatusTypeFailed - run.EndedAt = time.Now() - run.Logs = processor.Log(ctx) - run.Error = err.Error() - - if err := s.repo.SaveRun(ctx, run); err != nil { - app.GetLogger().Error("failed to save workflow run", "err", err) - } - - return fmt.Errorf("failed to run workflow: %w", err) - } - - // 保存日志 - logs := processor.Log(ctx) - runStatus := domain.WorkflowRunStatusTypeSucceeded - runError := domain.WorkflowRunLogs(logs).FirstError() - if runError != "" { - runStatus = domain.WorkflowRunStatusTypeFailed - } - run.Status = runStatus - run.EndedAt = time.Now() - run.Logs = processor.Log(ctx) - run.Error = runError - if err := s.repo.SaveRun(ctx, run); err != nil { - app.GetLogger().Error("failed to save workflow run", "err", err) - return err - } - - return nil + return errors.New("TODO: 尚未实现") } func (s *WorkflowService) Stop(ctx context.Context) { s.cancel() s.wg.Wait() } + +func (s *WorkflowService) run(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) + } + case <-ctx.Done(): + return + } + } +} + +func (s *WorkflowService) runWithData(ctx context.Context, runData *workflowRunData) error { + workflow := runData.Workflow + run := &domain.WorkflowRun{ + WorkflowId: workflow.Id, + Status: domain.WorkflowRunStatusTypeRunning, + Trigger: runData.RunTrigger, + StartedAt: time.Now(), + } + if resp, err := s.repo.SaveRun(ctx, run); err != nil { + return err + } else { + run = resp + } + + processor := processor.NewWorkflowProcessor(workflow) + if runErr := processor.Run(ctx); runErr != nil { + run.Status = domain.WorkflowRunStatusTypeFailed + run.EndedAt = time.Now() + run.Logs = processor.GetRunLogs() + run.Error = runErr.Error() + if _, err := s.repo.SaveRun(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.GetRunLogs() + 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 { + app.GetLogger().Error("failed to save workflow run", "err", err) + return err + } + + return nil +} diff --git a/migrations/1737479489_updated_workflow.go b/migrations/1737479489_updated_workflow.go new file mode 100644 index 00000000..7e06df36 --- /dev/null +++ b/migrations/1737479489_updated_workflow.go @@ -0,0 +1,65 @@ +package migrations + +import ( + "github.com/pocketbase/pocketbase/core" + m "github.com/pocketbase/pocketbase/migrations" +) + +func init() { + m.Register(func(app core.App) error { + collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur") + if err != nil { + return err + } + + // update field + if err := collection.Fields.AddMarshaledJSONAt(10, []byte(`{ + "hidden": false, + "id": "zivdxh23", + "maxSelect": 1, + "name": "lastRunStatus", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "pending", + "running", + "succeeded", + "failed", + "canceled" + ] + }`)); err != nil { + return err + } + + return app.Save(collection) + }, func(app core.App) error { + collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur") + if err != nil { + return err + } + + // update field + if err := collection.Fields.AddMarshaledJSONAt(10, []byte(`{ + "hidden": false, + "id": "zivdxh23", + "maxSelect": 1, + "name": "lastRunStatus", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "pending", + "running", + "succeeded", + "failed" + ] + }`)); err != nil { + return err + } + + return app.Save(collection) + }) +} diff --git a/migrations/1737479538_updated_workflow_run.go b/migrations/1737479538_updated_workflow_run.go new file mode 100644 index 00000000..b5ff20a7 --- /dev/null +++ b/migrations/1737479538_updated_workflow_run.go @@ -0,0 +1,65 @@ +package migrations + +import ( + "github.com/pocketbase/pocketbase/core" + m "github.com/pocketbase/pocketbase/migrations" +) + +func init() { + m.Register(func(app core.App) error { + collection, err := app.FindCollectionByNameOrId("qjp8lygssgwyqyz") + if err != nil { + return err + } + + // update field + if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{ + "hidden": false, + "id": "qldmh0tw", + "maxSelect": 1, + "name": "status", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "pending", + "running", + "succeeded", + "failed", + "canceled" + ] + }`)); err != nil { + return err + } + + return app.Save(collection) + }, func(app core.App) error { + collection, err := app.FindCollectionByNameOrId("qjp8lygssgwyqyz") + if err != nil { + return err + } + + // update field + if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{ + "hidden": false, + "id": "qldmh0tw", + "maxSelect": 1, + "name": "status", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "pending", + "running", + "succeeded", + "failed" + ] + }`)); err != nil { + return err + } + + return app.Save(collection) + }) +} diff --git a/ui/package-lock.json b/ui/package-lock.json index 97dcc7fe..394aebcd 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -13,6 +13,7 @@ "ahooks": "^3.8.4", "antd": "^5.23.1", "antd-zod": "^6.0.1", + "clsx": "^2.1.1", "cron-parser": "^4.9.0", "file-saver": "^2.0.5", "i18next": "^24.2.1", @@ -27,6 +28,7 @@ "react-dom": "^18.3.1", "react-i18next": "^15.4.0", "react-router-dom": "^7.1.3", + "tailwind-merge": "^2.6.0", "zod": "^3.24.1", "zustand": "^5.0.3" }, @@ -4124,6 +4126,14 @@ "resolved": "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz", "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", @@ -8563,6 +8573,15 @@ "integrity": "sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==", "dev": true }, + "node_modules/tailwind-merge": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz", + "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "3.4.17", "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.17.tgz", diff --git a/ui/package.json b/ui/package.json index c5cafafc..f2b1e993 100644 --- a/ui/package.json +++ b/ui/package.json @@ -15,6 +15,7 @@ "ahooks": "^3.8.4", "antd": "^5.23.1", "antd-zod": "^6.0.1", + "clsx": "^2.1.1", "cron-parser": "^4.9.0", "file-saver": "^2.0.5", "i18next": "^24.2.1", @@ -29,6 +30,7 @@ "react-dom": "^18.3.1", "react-i18next": "^15.4.0", "react-router-dom": "^7.1.3", + "tailwind-merge": "^2.6.0", "zod": "^3.24.1", "zustand": "^5.0.3" }, diff --git a/ui/src/api/certificates.ts b/ui/src/api/certificates.ts index 2a839b5a..3c00fdcf 100644 --- a/ui/src/api/certificates.ts +++ b/ui/src/api/certificates.ts @@ -3,10 +3,10 @@ import { ClientResponseError } from "pocketbase"; import { type CertificateFormatType } from "@/domain/certificate"; import { getPocketBase } from "@/repository/_pocketbase"; -export const archive = async (id: string, format?: CertificateFormatType) => { +export const archive = async (certificateId: string, format?: CertificateFormatType) => { const pb = getPocketBase(); - const resp = await pb.send(`/api/certificates/${encodeURIComponent(id)}/archive`, { + const resp = await pb.send>(`/api/certificates/${encodeURIComponent(certificateId)}/archive`, { method: "POST", headers: { "Content-Type": "application/json", @@ -38,9 +38,11 @@ export const validateCertificate = async (certificate: string) => { certificate: certificate, }, }); + if (resp.code != 0) { - throw new Error(resp.msg); + throw new ClientResponseError({ status: resp.code, response: resp, data: {} }); } + return resp; }; @@ -55,8 +57,10 @@ export const validatePrivateKey = async (privateKey: string) => { privateKey: privateKey, }, }); + if (resp.code != 0) { - throw new Error(resp.msg); + throw new ClientResponseError({ status: resp.code, response: resp, data: {} }); } + return resp; }; diff --git a/ui/src/api/workflows.ts b/ui/src/api/workflows.ts index 4ad9e2d3..6a9d69f6 100644 --- a/ui/src/api/workflows.ts +++ b/ui/src/api/workflows.ts @@ -3,10 +3,10 @@ import { ClientResponseError } from "pocketbase"; import { WORKFLOW_TRIGGERS } from "@/domain/workflow"; import { getPocketBase } from "@/repository/_pocketbase"; -export const run = async (id: string) => { +export const startRun = async (workflowId: string) => { const pb = getPocketBase(); - const resp = await pb.send(`/api/workflows/${encodeURIComponent(id)}/run`, { + const resp = await pb.send(`/api/workflows/${encodeURIComponent(workflowId)}/runs`, { method: "POST", headers: { "Content-Type": "application/json", @@ -22,3 +22,20 @@ export const run = async (id: string) => { return resp; }; + +export const cancelRun = async (workflowId: string, runId: string) => { + const pb = getPocketBase(); + + const resp = await pb.send(`/api/workflows/${encodeURIComponent(workflowId)}/runs/${encodeURIComponent(runId)}/cancel`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + + if (resp.code != 0) { + throw new ClientResponseError({ status: resp.code, response: resp, data: {} }); + } + + return resp; +}; diff --git a/ui/src/components/DrawerForm.tsx b/ui/src/components/DrawerForm.tsx index 0c319fb6..46116095 100644 --- a/ui/src/components/DrawerForm.tsx +++ b/ui/src/components/DrawerForm.tsx @@ -49,9 +49,7 @@ const DrawerForm = = any>({ const triggerEl = useTriggerElement(trigger, { onClick: () => { - console.log("click"); setOpen(true); - console.log(open); }, }); diff --git a/ui/src/components/certificate/CertificateDetail.tsx b/ui/src/components/certificate/CertificateDetail.tsx index b782afb5..6feb992b 100644 --- a/ui/src/components/certificate/CertificateDetail.tsx +++ b/ui/src/components/certificate/CertificateDetail.tsx @@ -27,7 +27,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => { const blob = new Blob([u8arr], { type: "application/zip" }); saveAs(blob, `${data.id}-${data.subjectAltNames}.zip`); } catch (err) { - console.log(err); + console.error(err); messageApi.warning(t("common.text.operation_failed")); } }; diff --git a/ui/src/components/workflow/WorkflowElementsContainer.tsx b/ui/src/components/workflow/WorkflowElementsContainer.tsx new file mode 100644 index 00000000..2329eeac --- /dev/null +++ b/ui/src/components/workflow/WorkflowElementsContainer.tsx @@ -0,0 +1,41 @@ +import { useState } from "react"; +import { ExpandOutlined as ExpandOutlinedIcon, MinusOutlined as MinusOutlinedIcon, PlusOutlined as PlusOutlinedIcon } from "@ant-design/icons"; +import { Button, Card, Typography } from "antd"; + +import WorkflowElements from "@/components/workflow/WorkflowElements"; +import { mergeCls } from "@/utils/css"; + +export type WorkflowElementsProps = { + className?: string; + style?: React.CSSProperties; + disabled?: boolean; +}; + +const WorkflowElementsContainer = ({ className, style, disabled }: WorkflowElementsProps) => { + const [scale, setScale] = useState(1); + + return ( +
+
+
+
+
+ +
+
+
+
+ + +
+
+
+
+ ); +}; + +export default WorkflowElementsContainer; diff --git a/ui/src/components/workflow/WorkflowRunDetailDrawer.tsx b/ui/src/components/workflow/WorkflowRunDetailDrawer.tsx index 5c052aa1..11e2847c 100644 --- a/ui/src/components/workflow/WorkflowRunDetailDrawer.tsx +++ b/ui/src/components/workflow/WorkflowRunDetailDrawer.tsx @@ -41,11 +41,11 @@ const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowR
-
+
{data!.logs?.map((item, i) => { return (
-
{item.nodeName}
+
{item.nodeName}
{item.outputs?.map((output, j) => { return ( diff --git a/ui/src/components/workflow/WorkflowRuns.tsx b/ui/src/components/workflow/WorkflowRuns.tsx index ef9fd5ea..c90ab5a3 100644 --- a/ui/src/components/workflow/WorkflowRuns.tsx +++ b/ui/src/components/workflow/WorkflowRuns.tsx @@ -4,17 +4,21 @@ import { CheckCircleOutlined as CheckCircleOutlinedIcon, ClockCircleOutlined as ClockCircleOutlinedIcon, CloseCircleOutlined as CloseCircleOutlinedIcon, + DeleteOutlined as DeleteOutlinedIcon, + PauseCircleOutlined as PauseCircleOutlinedIcon, + PauseOutlined as PauseOutlinedIcon, SelectOutlined as SelectOutlinedIcon, SyncOutlined as SyncOutlinedIcon, } from "@ant-design/icons"; import { useRequest } from "ahooks"; -import { Button, Empty, Table, type TableProps, Tag, notification } from "antd"; +import { Button, Empty, Modal, Table, type TableProps, Tag, Tooltip, notification } from "antd"; import dayjs from "dayjs"; 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 } from "@/repository/workflowRun"; +import { list as listWorkflowRuns, remove as removeWorkflowRun } from "@/repository/workflowRun"; import { getErrMsg } from "@/utils/error"; import WorkflowRunDetailDrawer from "./WorkflowRunDetailDrawer"; @@ -27,6 +31,7 @@ export type WorkflowRunsProps = { const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => { const { t } = useTranslation(); + const [modalApi, ModelContextHolder] = Modal.useModal(); const [notificationApi, NotificationContextHolder] = notification.useNotification(); const tableColumns: TableProps["columns"] = [ @@ -68,6 +73,12 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => { {t("workflow_run.props.status.failed")} ); + } else if (record.status === WORKFLOW_RUN_STATUSES.CANCELED) { + return ( + } color="warning"> + {t("workflow_run.props.status.canceled")} + + ); } return <>; @@ -116,11 +127,51 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => { align: "end", fixed: "right", width: 120, - render: (_, record) => ( - - } variant="text" />} /> - - ), + render: (_, record) => { + const allowCancel = record.status === WORKFLOW_RUN_STATUSES.PENDING || record.status === WORKFLOW_RUN_STATUSES.RUNNING; + const aloowDelete = + record.status === WORKFLOW_RUN_STATUSES.SUCCEEDED || + record.status === WORKFLOW_RUN_STATUSES.FAILED || + record.status === WORKFLOW_RUN_STATUSES.CANCELED; + + return ( + + + - -
+ + + + + false} maxCount={1} onChange={handleCertificateFileChange}> + + - -
- false} maxCount={1} onChange={handlePrivateKeyFileChange}> - - -
+ +
+ + + false} maxCount={1} onChange={handlePrivateKeyFileChange}> + + ); diff --git a/ui/src/components/workflow/node/_SharedNode.tsx b/ui/src/components/workflow/node/_SharedNode.tsx index 868481ef..f2a4df60 100644 --- a/ui/src/components/workflow/node/_SharedNode.tsx +++ b/ui/src/components/workflow/node/_SharedNode.tsx @@ -64,6 +64,16 @@ type SharedNodeMenuProps = SharedNodeProps & { afterDelete?: () => void; }; +const isBranchingNode = (node: WorkflowNode) => { + return ( + node.type === WorkflowNodeType.Branch || + node.type === WorkflowNodeType.Condition || + node.type === WorkflowNodeType.ExecuteResultBranch || + node.type === WorkflowNodeType.ExecuteSuccess || + node.type === WorkflowNodeType.ExecuteFailure + ); +}; + const SharedNodeMenu = ({ trigger, node, disabled, branchId, branchIndex, afterUpdate, afterDelete }: SharedNodeMenuProps) => { const { t } = useTranslation(); @@ -91,13 +101,7 @@ const SharedNodeMenu = ({ trigger, node, disabled, branchId, branchIndex, afterU }; const handleDeleteClick = async () => { - if ( - node.type === WorkflowNodeType.Branch || - node.type === WorkflowNodeType.Condition || - node.type === WorkflowNodeType.ExecuteResultBranch || - node.type === WorkflowNodeType.ExecuteSuccess || - node.type === WorkflowNodeType.ExecuteFailure - ) { + if (isBranchingNode(node)) { await removeBranch(branchId!, branchIndex!); } else { await removeNode(node.id); @@ -116,19 +120,13 @@ const SharedNodeMenu = ({ trigger, node, disabled, branchId, branchIndex, afterU { key: "rename", disabled: disabled, - label: - node.type === WorkflowNodeType.Branch || node.type === WorkflowNodeType.Condition - ? t("workflow_node.action.rename_branch") - : t("workflow_node.action.rename_node"), + label: isBranchingNode(node) ? t("workflow_node.action.rename_branch") : t("workflow_node.action.rename_node"), icon: , onClick: () => { nameRef.current = node.name; const dialog = modalApi.confirm({ - title: - node.type === WorkflowNodeType.Branch || node.type === WorkflowNodeType.Condition - ? t("workflow_node.action.rename_branch") - : t("workflow_node.action.rename_node"), + title: isBranchingNode(node) ? t("workflow_node.action.rename_branch") : t("workflow_node.action.rename_node"), content: (
, danger: true, onClick: handleDeleteClick, @@ -193,10 +184,10 @@ const SharedNodeBlock = ({ children, node, disabled, onClick }: SharedNodeBlockP return ( <> } variant="text" />} />} - overlayClassName="shadow-md" - overlayInnerStyle={{ padding: 0 }} placement="rightTop" > diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index b1296d9d..d001f2e9 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -29,30 +29,30 @@ export type WorkflowTriggerType = (typeof WORKFLOW_TRIGGERS)[keyof typeof WORKFL export enum WorkflowNodeType { Start = "start", End = "end", - Branch = "branch", - ExecuteResultBranch = "execute_result_branch", - ExecuteSuccess = "execute_success", - ExecuteFailure = "execute_failure", - Condition = "condition", Apply = "apply", Upload = "upload", Deploy = "deploy", Notify = "notify", + Branch = "branch", + Condition = "condition", + ExecuteResultBranch = "execute_result_branch", + ExecuteSuccess = "execute_success", + ExecuteFailure = "execute_failure", Custom = "custom", } const workflowNodeTypeDefaultNames: Map = new Map([ [WorkflowNodeType.Start, i18n.t("workflow_node.start.label")], [WorkflowNodeType.End, i18n.t("workflow_node.end.label")], - [WorkflowNodeType.Branch, i18n.t("workflow_node.branch.label")], - [WorkflowNodeType.ExecuteResultBranch, i18n.t("workflow_node.execute_result_branch.label")], - [WorkflowNodeType.ExecuteSuccess, i18n.t("workflow_node.execute_success.label")], - [WorkflowNodeType.ExecuteFailure, i18n.t("workflow_node.execute_failure.label")], - [WorkflowNodeType.Condition, i18n.t("workflow_node.condition.label")], [WorkflowNodeType.Apply, i18n.t("workflow_node.apply.label")], [WorkflowNodeType.Upload, i18n.t("workflow_node.upload.label")], [WorkflowNodeType.Deploy, i18n.t("workflow_node.deploy.label")], [WorkflowNodeType.Notify, i18n.t("workflow_node.notify.label")], + [WorkflowNodeType.Branch, i18n.t("workflow_node.branch.label")], + [WorkflowNodeType.Condition, i18n.t("workflow_node.condition.label")], + [WorkflowNodeType.ExecuteResultBranch, i18n.t("workflow_node.execute_result_branch.label")], + [WorkflowNodeType.ExecuteSuccess, i18n.t("workflow_node.execute_success.label")], + [WorkflowNodeType.ExecuteFailure, i18n.t("workflow_node.execute_failure.label")], [WorkflowNodeType.Custom, i18n.t("workflow_node.custom.title")], ]); diff --git a/ui/src/domain/workflowRun.ts b/ui/src/domain/workflowRun.ts index eaac90b4..55b5f36e 100644 --- a/ui/src/domain/workflowRun.ts +++ b/ui/src/domain/workflowRun.ts @@ -32,6 +32,7 @@ export const WORKFLOW_RUN_STATUSES = Object.freeze({ RUNNING: "running", SUCCEEDED: "succeeded", FAILED: "failed", + CANCELED: "canceled", } as const); export type WorkflorRunStatusType = (typeof WORKFLOW_RUN_STATUSES)[keyof typeof WORKFLOW_RUN_STATUSES]; diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 87207597..edd26b74 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -1,5 +1,5 @@ { - "workflow_node.action.configure_node": "Configure", + "workflow_node.action.configure_node": "Configure node", "workflow_node.action.add_node": "Add node", "workflow_node.action.rename_node": "Rename node", "workflow_node.action.remove_node": "Delete node", @@ -20,7 +20,7 @@ "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.extra": "Expected execution time for the last 5 times:", - "workflow_node.start.form.trigger_cron_alert.content": "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?", + "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?", "workflow_node.apply.label": "Application", "workflow_node.apply.form.domains.label": "Domains", @@ -82,6 +82,7 @@ "workflow_node.deploy.form.provider_access.placeholder": "Please select an authorization of host provider", "workflow_node.deploy.form.provider_access.tooltip": "Used to deploy certificates.", "workflow_node.deploy.form.provider_access.button": "Create", + "workflow_node.deploy.form.provider_access.guide_for_local": "Tips: Due to the form validations, youe need to select an authorization for local deployment also, even if it means nothing.", "workflow_node.deploy.form.certificate.label": "Certificate", "workflow_node.deploy.form.certificate.placeholder": "Please select certificate", "workflow_node.deploy.form.certificate.tooltip": "The certificate to be deployed comes from the previous application stage node.", @@ -377,25 +378,25 @@ "workflow_node.notify.form.channel.placeholder": "Please select channel", "workflow_node.notify.form.channel.button": "Configure", - "workflow_node.upload.label": "Upload certificate", + "workflow_node.upload.label": "Upload", "workflow_node.upload.form.domains.label": "Domains", - "workflow_node.upload.form.certificate.label": "Certificate", - "workflow_node.upload.form.certificate.placeholder": "The certificate format begins with \"-----BEGIN CERTIFICATE-----\" and ends with \"-----END CERTIFICATE-----\"", - "workflow_node.upload.form.certificate.button": "Upload", - "workflow_node.upload.form.private_key.label": "Private key", - "workflow_node.upload.form.private_key.placeholder": "The private key begins with \"-----BEGIN (RSA|EC) PRIVATE KEY-----\" and ends with \"-----END(RSA|EC) PRIVATE KEY-----\"", - "workflow_node.upload.form.private_key.button": "Upload", + "workflow_node.upload.form.domains.placholder": "Please select certificate file", + "workflow_node.upload.form.certificate.label": "Certificate (PEM format)", + "workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----", + "workflow_node.upload.form.certificate.button": "Choose file ...", + "workflow_node.upload.form.private_key.label": "Private key (PEM format)", + "workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----", + "workflow_node.upload.form.private_key.button": "Choose file ...", "workflow_node.end.label": "End", - "workflow_node.branch.label": "Branch", + "workflow_node.branch.label": "Parallel branch", - "workflow_node.execute_result_branch.label": "Execute result branch", + "workflow_node.condition.label": "Branch", - "workflow_node.execute_success.label": "Execute success", + "workflow_node.execute_result_branch.label": "Execution result branch", - "workflow_node.execute_failure.label": "Execute failure", + "workflow_node.execute_success.label": "If the previous node succeeded ...", - "workflow_node.condition.label": "Condition" + "workflow_node.execute_failure.label": "If the previous node failed ..." } - diff --git a/ui/src/i18n/locales/en/nls.workflow.runs.json b/ui/src/i18n/locales/en/nls.workflow.runs.json index 660cdecb..c58d4ad9 100644 --- a/ui/src/i18n/locales/en/nls.workflow.runs.json +++ b/ui/src/i18n/locales/en/nls.workflow.runs.json @@ -1,10 +1,17 @@ { + "workflow_run.action.view": "View detail", + "workflow_run.action.cancel": "Cancel run", + "workflow_run.action.cancel.confirm": "Are you sure to cancel this run?", + "workflow_run.action.delete": "Delete run", + "workflow_run.action.delete.confirm": "Are you sure to delete this run?", + "workflow_run.props.id": "ID", "workflow_run.props.status": "Status", "workflow_run.props.status.pending": "Pending", "workflow_run.props.status.running": "Running", "workflow_run.props.status.succeeded": "Succeeded", "workflow_run.props.status.failed": "Failed", + "workflow_run.props.status.canceled": "Canceled", "workflow_run.props.trigger": "Trigger", "workflow_run.props.trigger.auto": "Timing", "workflow_run.props.trigger.manual": "Manual", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 609e96ee..4eddbd92 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -3,7 +3,7 @@ "workflow_node.branch.add_node": "添加节点", "workflow_node.action.rename_node": "重命名", "workflow_node.action.remove_node": "删除节点", - "workflow_node.action.add_branch": "添加分支", + "workflow_node.action.add_branch": "添加并行分支", "workflow_node.action.rename_branch": "重命名", "workflow_node.action.remove_branch": "删除分支", @@ -20,7 +20,7 @@ "workflow_node.start.form.trigger_cron.errmsg.invalid": "请输入正确的 Cron 表达式", "workflow_node.start.form.trigger_cron.tooltip": "支持使用任意值(即 *)、值列表分隔符(即 ,)、值的范围(即 -)、步骤值(即 /)等四种表达式,时区以服务器设置为准。", "workflow_node.start.form.trigger_cron.extra": "预计最近 5 次执行时间:", - "workflow_node.start.form.trigger_cron_alert.content": "小贴士:如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。

参考链接:
1. Let’s Encrypt 速率限制
2. 为什么我的 Let’s Encrypt (ACME) 客户端启动时间应当随机?", + "workflow_node.start.form.trigger_cron.guide": "小贴士:如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。

参考链接:
1. Let’s Encrypt 速率限制
2. 为什么我的 Let’s Encrypt (ACME) 客户端启动时间应当随机?", "workflow_node.apply.label": "申请", "workflow_node.apply.form.domains.label": "域名", @@ -37,6 +37,7 @@ "workflow_node.apply.form.provider_access.placeholder": "请选择 DNS 提供商授权", "workflow_node.apply.form.provider_access.tooltip": "用于 ACME DNS-01 认证时操作域名解析记录,注意与部署阶段所需的主机提供商相区分。", "workflow_node.apply.form.provider_access.button": "新建", + "workflow_node.deploy.form.provider_access.guide_for_local": "小贴士:由于表单限制,你同样需要为本地部署选择一个授权 —— 即使它是空白的。", "workflow_node.apply.form.aws_route53_region.label": "AWS Route53 区域", "workflow_node.apply.form.aws_route53_region.placeholder": "请输入 AWS Route53 区域(例如:us-east-1)", "workflow_node.apply.form.aws_route53_region.tooltip": "这是什么?请参阅 https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints", @@ -377,25 +378,25 @@ "workflow_node.notify.form.channel.placeholder": "请选择通知渠道", "workflow_node.notify.form.channel.button": "去配置", - "workflow_node.upload.label": "上传证书", - "workflow_node.upload.form.domains.label": "证书域名", - "workflow_node.upload.form.certificate.label": "证书文件", - "workflow_node.upload.form.certificate.placeholder": "证书格式以\"-----BEGIN CERTIFICATE-----\"开头,以\"-----END CERTIFICATE-----\"结尾。", - "workflow_node.upload.form.certificate.button": "上传", - "workflow_node.upload.form.private_key.label": "证书私钥", - "workflow_node.upload.form.private_key.placeholder": "证书私钥格式以\"-----BEGIN (RSA|EC) PRIVATE KEY-----\"开头,以\"-----END(RSA|EC) PRIVATE KEY-----\"结尾。", - "workflow_node.upload.form.private_key.button": "上传", + "workflow_node.upload.label": "上传", + "workflow_node.upload.form.domains.label": "域名", + "workflow_node.upload.form.domains.placeholder": "上传证书文件后显示", + "workflow_node.upload.form.certificate.label": "证书文件(PEM 格式)", + "workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----", + "workflow_node.upload.form.certificate.button": "选择文件", + "workflow_node.upload.form.private_key.label": "私钥文件(PEM 格式)", + "workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----", + "workflow_node.upload.form.private_key.button": "选择文件", "workflow_node.end.label": "结束", - "workflow_node.branch.label": "分支", + "workflow_node.branch.label": "并行分支", + + "workflow_node.condition.label": "分支", "workflow_node.execute_result_branch.label": "执行结果分支", - "workflow_node.execute_success.label": "执行成功", + "workflow_node.execute_success.label": "若前序节点执行成功…", - "workflow_node.execute_failure.label": "执行失败", - - "workflow_node.condition.label": "条件" + "workflow_node.execute_failure.label": "若前序节点执行失败…" } - diff --git a/ui/src/i18n/locales/zh/nls.workflow.runs.json b/ui/src/i18n/locales/zh/nls.workflow.runs.json index caa4bde2..762a1196 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.runs.json +++ b/ui/src/i18n/locales/zh/nls.workflow.runs.json @@ -1,10 +1,17 @@ { + "workflow_run.action.view": "查看详情", + "workflow_run.action.cancel": "取消执行", + "workflow_run.action.cancel.confirm": "确定要取消此执行吗?请注意此操作仅中止流程,但不会回滚已执行的节点。", + "workflow_run.action.delete": "删除执行", + "workflow_run.action.delete.confirm": "确定要删除此执行吗?请注意此操作仅清除日志历史,但不会影响各节点的执行结果和签发的证书。", + "workflow_run.props.id": "ID", "workflow_run.props.status": "状态", "workflow_run.props.status.pending": "等待执行", "workflow_run.props.status.running": "执行中", - "workflow_run.props.status.succeeded": "成功", - "workflow_run.props.status.failed": "失败", + "workflow_run.props.status.succeeded": "已成功", + "workflow_run.props.status.failed": "已失败", + "workflow_run.props.status.canceled": "已取消", "workflow_run.props.trigger": "执行方式", "workflow_run.props.trigger.auto": "定时执行", "workflow_run.props.trigger.manual": "手动执行", diff --git a/ui/src/pages/ConsoleLayout.tsx b/ui/src/pages/ConsoleLayout.tsx index a370d46c..4fbaf492 100644 --- a/ui/src/pages/ConsoleLayout.tsx +++ b/ui/src/pages/ConsoleLayout.tsx @@ -41,7 +41,7 @@ const ConsoleLayout = () => { } return ( - +
@@ -53,8 +53,8 @@ const ConsoleLayout = () => {
- - + +
} size="large" />} /> @@ -76,7 +76,7 @@ const ConsoleLayout = () => {
- + diff --git a/ui/src/pages/dashboard/Dashboard.tsx b/ui/src/pages/dashboard/Dashboard.tsx index be6323dd..d80c8c10 100644 --- a/ui/src/pages/dashboard/Dashboard.tsx +++ b/ui/src/pages/dashboard/Dashboard.tsx @@ -2,15 +2,16 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { - ApiOutlined, - CheckCircleOutlined, - ClockCircleOutlined, - CloseCircleOutlined, - LockOutlined, - PlusOutlined, - SelectOutlined, - SendOutlined, - SyncOutlined, + ApiOutlined as ApiOutlinedIcon, + CheckCircleOutlined as CheckCircleOutlinedIcon, + ClockCircleOutlined as ClockCircleOutlinedIcon, + CloseCircleOutlined as CloseCircleOutlinedIcon, + LockOutlined as LockOutlinedIcon, + PauseCircleOutlined as PauseCircleOutlinedIcon, + PlusOutlined as PlusOutlinedIcon, + SelectOutlined as SelectOutlinedIcon, + SendOutlined as SendOutlinedIcon, + SyncOutlined as SyncOutlinedIcon, } from "@ant-design/icons"; import { PageHeader } from "@ant-design/pro-components"; import { useRequest } from "ahooks"; @@ -84,14 +85,22 @@ const Dashboard = () => { key: "name", title: t("workflow.props.name"), ellipsis: true, - render: (_, record) => ( - - {record.expand?.workflowId?.name} - - {record.expand?.workflowId?.description} - - - ), + render: (_, record) => { + const workflow = record.expand?.workflowId; + return ( + { + if (workflow) { + navigate(`/workflows/${workflow.id}`); + } + }} + > + {workflow?.name ?? {t(`#${record.workflowId}`)}} + + ); + }, }, { key: "status", @@ -99,25 +108,31 @@ const Dashboard = () => { ellipsis: true, render: (_, record) => { if (record.status === WORKFLOW_RUN_STATUSES.PENDING) { - return }>{t("workflow_run.props.status.pending")}; + return }>{t("workflow_run.props.status.pending")}; } else if (record.status === WORKFLOW_RUN_STATUSES.RUNNING) { return ( - } color="processing"> + } color="processing"> {t("workflow_run.props.status.running")} ); } else if (record.status === WORKFLOW_RUN_STATUSES.SUCCEEDED) { return ( - } color="success"> + } color="success"> {t("workflow_run.props.status.succeeded")} ); } else if (record.status === WORKFLOW_RUN_STATUSES.FAILED) { return ( - } color="error"> + } color="error"> {t("workflow_run.props.status.failed")} ); + } else if (record.status === WORKFLOW_RUN_STATUSES.CANCELED) { + return ( + } color="warning"> + {t("workflow_run.props.status.canceled")} + + ); } return <>; @@ -153,7 +168,7 @@ const Dashboard = () => { width: 120, render: (_, record) => ( - } variant="text" />} /> + } variant="text" />} /> ), }, @@ -248,16 +263,16 @@ const Dashboard = () => { - - - - diff --git a/ui/src/pages/workflows/WorkflowDetail.tsx b/ui/src/pages/workflows/WorkflowDetail.tsx index cdc1379e..f85394d1 100644 --- a/ui/src/pages/workflows/WorkflowDetail.tsx +++ b/ui/src/pages/workflows/WorkflowDetail.tsx @@ -8,9 +8,6 @@ import { DownOutlined as DownOutlinedIcon, EllipsisOutlined as EllipsisOutlinedIcon, HistoryOutlined as HistoryOutlinedIcon, - MinusOutlined, - PlusCircleOutlined, - ReloadOutlined, UndoOutlined as UndoOutlinedIcon, } from "@ant-design/icons"; import { PageHeader } from "@ant-design/pro-components"; @@ -19,10 +16,10 @@ import { createSchemaFieldRule } from "antd-zod"; import { isEqual } from "radash"; import { z } from "zod"; -import { run as runWorkflow } from "@/api/workflows"; +import { startRun as startWorkflowRun } from "@/api/workflows"; import ModalForm from "@/components/ModalForm"; import Show from "@/components/Show"; -import WorkflowElements from "@/components/workflow/WorkflowElements"; +import WorkflowElementsContainer from "@/components/workflow/WorkflowElementsContainer"; import WorkflowRuns from "@/components/workflow/WorkflowRuns"; import { isAllNodesValidated } from "@/domain/workflow"; import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun"; @@ -40,8 +37,6 @@ const WorkflowDetail = () => { const [modalApi, ModalContextHolder] = Modal.useModal(); const [notificationApi, NotificationContextHolder] = notification.useNotification(); - const [scale, setScale] = useState(1); - const { id: workflowId } = useParams(); const { workflow, initialized, ...workflowState } = useWorkflowStore( useZustandShallowSelector(["workflow", "initialized", "init", "destroy", "setEnabled", "release", "discard"]) @@ -58,15 +53,12 @@ const WorkflowDetail = () => { const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration"); const [isRunning, setIsRunning] = useState(false); + const lastRunStatus = useMemo(() => workflow.lastRunStatus, [workflow]); const [allowDiscard, setAllowDiscard] = useState(false); const [allowRelease, setAllowRelease] = useState(false); const [allowRun, setAllowRun] = useState(false); - const lastRunStatus = useMemo(() => { - return workflow.lastRunStatus; - }, [workflow]); - useEffect(() => { setIsRunning(lastRunStatus == WORKFLOW_RUN_STATUSES.RUNNING); }, [lastRunStatus]); @@ -192,7 +184,7 @@ const WorkflowDetail = () => { } }); - await runWorkflow(workflowId!); + await startWorkflowRun(workflowId!); messageApi.info(t("workflow.detail.orchestration.action.run.prompt")); } catch (err) { @@ -206,123 +198,129 @@ const WorkflowDetail = () => { }; return ( -
+
{MessageContextHolder} {ModalContextHolder} {NotificationContextHolder} - - {t("common.button.edit")}} />, +
+ + {t("common.button.edit")}} />, - , + , - , - onClick: () => { - handleDeleteClick(); - }, - }, - ], - }} - trigger={["click"]} - > - - , - ] - : [] - } - > - {workflow.description} - }, - { key: "runs", label: t("workflow.detail.runs.tab"), icon: }, - ]} - renderTabBar={(props, DefaultTabBar) => } - tabBarStyle={{ border: "none" }} - onChange={(key) => setTabValue(key as typeof tabValue)} - /> - - - -
- - -
-
-
- - {t("workflow.detail.orchestration.draft.alert")}
} type="warning" /> - -
-
- - - - - - - , - onClick: handleDiscardClick, + , + onClick: () => { + handleDeleteClick(); }, - ], - }} - trigger={["click"]} - > -
-
-
-
- -
- -
-
- - - - - + }, + ], + }} + trigger={["click"]} + > + + , + ] + : [] + } + > + {workflow.description} + }, + { key: "runs", label: t("workflow.detail.runs.tab"), icon: }, + ]} + renderTabBar={(props, DefaultTabBar) => } + tabBarStyle={{ border: "none" }} + onChange={(key) => setTabValue(key as typeof tabValue)} + /> +
+ + +
+ +
+
+ + {t("workflow.detail.orchestration.draft.alert")}
} type="warning" /> + +
+
+ + + + + + + , + onClick: handleDiscardClick, + }, + ], + }} + trigger={["click"]} + > +
+
+ + +
+
+ + + +
+ + + +
+
); }; diff --git a/ui/src/pages/workflows/WorkflowList.tsx b/ui/src/pages/workflows/WorkflowList.tsx index a1fce6c1..28b776e7 100644 --- a/ui/src/pages/workflows/WorkflowList.tsx +++ b/ui/src/pages/workflows/WorkflowList.tsx @@ -3,9 +3,11 @@ import { useTranslation } from "react-i18next"; import { useNavigate, useSearchParams } from "react-router-dom"; import { CheckCircleOutlined as CheckCircleOutlinedIcon, + ClockCircleOutlined as ClockCircleOutlinedIcon, CloseCircleOutlined as CloseCircleOutlinedIcon, DeleteOutlined as DeleteOutlinedIcon, EditOutlined as EditOutlinedIcon, + PauseCircleOutlined as PauseCircleOutlinedIcon, PlusOutlined as PlusOutlinedIcon, SyncOutlined as SyncOutlinedIcon, } from "@ant-design/icons"; @@ -13,7 +15,6 @@ import { import { PageHeader } from "@ant-design/pro-components"; import { useRequest } from "ahooks"; import { - Badge, Button, Divider, Empty, @@ -159,32 +160,25 @@ const WorkflowList = () => { key: "lastRun", title: t("workflow.props.last_run_at"), render: (_, record) => { - if (record.lastRunId) { - if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.RUNNING) { - return ( - - } /> - {dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss")} - - ); - } else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.SUCCEEDED) { - return ( - - } /> - {dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss")} - - ); - } else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.FAILED) { - return ( - - } /> - {dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss")} - - ); - } + let icon = <>; + if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.PENDING) { + icon = ; + } else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.RUNNING) { + icon = ; + } else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.SUCCEEDED) { + icon = ; + } else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.FAILED) { + icon = ; + } else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.CANCELED) { + icon = ; } - return <>; + return ( + + {icon} + {record.lastRunTime ? dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss") : ""} + + ); }, }, { diff --git a/ui/src/pages/workflows/WorkflowNew.tsx b/ui/src/pages/workflows/WorkflowNew.tsx index f24dde4c..68c5d96d 100644 --- a/ui/src/pages/workflows/WorkflowNew.tsx +++ b/ui/src/pages/workflows/WorkflowNew.tsx @@ -109,7 +109,7 @@ const WorkflowNew = () => {
{NotificationContextHolder} - + {t("workflow.new.subtitle")} diff --git a/ui/src/repository/workflowRun.ts b/ui/src/repository/workflowRun.ts index 25571eaf..0aa88080 100644 --- a/ui/src/repository/workflowRun.ts +++ b/ui/src/repository/workflowRun.ts @@ -14,7 +14,7 @@ export type ListWorkflowRunsRequest = { export const list = async (request: ListWorkflowRunsRequest) => { const page = request.page || 1; const perPage = request.perPage || 10; - console.log("request.workflowId", request.workflowId); + let filter = ""; const params: Record = {}; if (request.workflowId) { @@ -31,3 +31,7 @@ export const list = async (request: ListWorkflowRunsRequest) => { expand: request.expand ? "workflowId" : undefined, }); }; + +export const remove = async (record: MaybeModelRecordWithId) => { + return await getPocketBase().collection(COLLECTION_NAME).delete(record.id); +}; diff --git a/ui/src/utils/css.ts b/ui/src/utils/css.ts new file mode 100644 index 00000000..048bd5e6 --- /dev/null +++ b/ui/src/utils/css.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export const mergeCls = (...inputs: ClassValue[]) => { + return twMerge(clsx(inputs)); +}; diff --git a/ui/src/utils/error.ts b/ui/src/utils/error.ts index 0647d48b..df5465f4 100644 --- a/ui/src/utils/error.ts +++ b/ui/src/utils/error.ts @@ -1,5 +1,9 @@ +import { ClientResponseError } from "pocketbase"; + export const getErrMsg = (error: unknown): string => { - if (error instanceof Error) { + if (error instanceof ClientResponseError) { + return error.response != null ? getErrMsg(error.response) : error.message; + } else if (error instanceof Error) { return error.message; } else if (typeof error === "object" && error != null) { if ("message" in error) {