Merge branch 'feat/new-workflow' of github.com:fudiwei/certimate into next

This commit is contained in:
Yoan.liu 2025-02-12 09:42:00 +08:00
commit 138e08e985
97 changed files with 2288 additions and 1138 deletions

68
go.mod
View File

@ -28,7 +28,7 @@ require (
github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0 github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0
github.com/pkg/sftp v1.13.7 github.com/pkg/sftp v1.13.7
github.com/pocketbase/dbx v1.11.0 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/povsister/scp v0.0.0-20240802064259-28781e87b246
github.com/qiniu/go-sdk/v7 v7.25.2 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/cdn v1.0.1084
@ -42,7 +42,7 @@ require (
github.com/volcengine/volc-sdk-golang v1.0.193 github.com/volcengine/volc-sdk-golang v1.0.193
github.com/volcengine/volcengine-go-sdk v1.0.178 github.com/volcengine/volcengine-go-sdk v1.0.178
golang.org/x/crypto v0.32.0 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/api v0.32.1
k8s.io/apimachinery v0.32.1 k8s.io/apimachinery v0.32.1
k8s.io/client-go v0.32.1 k8s.io/client-go v0.32.1
@ -108,7 +108,6 @@ require (
) )
require ( require (
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect github.com/BurntSushi/toml v1.4.0 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/dcdn-20180115/v3 v3.5.0 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/alibaba-cloud-sdk-go v1.63.83 // indirect
github.com/aliyun/credentials-go v1.4.3 // indirect github.com/aliyun/credentials-go v1.4.3 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // 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 v1.36.0
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 // indirect
github.com/aws/aws-sdk-go-v2/config v1.29.1 github.com/aws/aws-sdk-go-v2/config v1.29.5
github.com/aws/aws-sdk-go-v2/credentials v1.17.54 github.com/aws/aws-sdk-go-v2/credentials v1.17.58
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 // indirect 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.52 // 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.28 // 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.28 // 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.1 // 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.28 // 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.1 // 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.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.9 // 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.9 // 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.73.2 // 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.11 // 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.10 // 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.9 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 // indirect
github.com/aws/smithy-go v1.22.1 // indirect github.com/aws/smithy-go v1.22.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudflare/cloudflare-go v0.114.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-jose/go-jose/v4 v4.0.4 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/goccy/go-json v0.10.4 // 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/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.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/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // 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/kr/fs v0.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // 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/miekg/dns v1.1.62 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // 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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/spf13/cast v1.7.1 // indirect github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/cobra v1.8.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/objx v0.5.2 // indirect
github.com/stretchr/testify v1.10.0 // indirect github.com/stretchr/testify v1.10.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1084 // indirect github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1084 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
gocloud.dev v0.40.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/mod v0.22.0 // indirect
golang.org/x/net v0.34.0 // indirect golang.org/x/net v0.34.0 // indirect
golang.org/x/oauth2 v0.25.0 // indirect golang.org/x/oauth2 v0.26.0 // indirect
golang.org/x/sync v0.10.0 golang.org/x/sync v0.11.0
golang.org/x/sys v0.29.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/term v0.28.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/time v0.9.0
golang.org/x/tools v0.29.0 // indirect golang.org/x/tools v0.29.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/api v0.217.0 // indirect google.golang.org/api v0.219.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489 // indirect
google.golang.org/grpc v1.69.4 // indirect google.golang.org/grpc v1.70.0 // indirect
google.golang.org/protobuf v1.36.3 // indirect google.golang.org/protobuf v1.36.4 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // 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/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.2 // indirect modernc.org/memory v1.8.2 // indirect
modernc.org/sqlite v1.34.5 // indirect modernc.org/sqlite v1.34.5 // indirect

156
go.sum
View File

@ -1,5 +1,5 @@
cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= cel.dev/expr v0.19.0 h1:lXuo+nDhpyJSpWxpPVi5cPUwzKb+dsdOiw6IreM5yt0=
cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= 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.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.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 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= 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 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 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 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 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= 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.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/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/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/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.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/sarama v1.30.1/go.mod h1:hGgx05L/DiW8XYBXeJdKIN6V2QUy2H6JqME5VT1NLRw= 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 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= 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.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.36.0 h1:b1wM5CcE65Ujwn565qcwgtOTT1aT4ADOHHgglKjG7fk=
github.com/aws/aws-sdk-go-v2 v1.33.0/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= 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.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8= 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.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc= 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.1 h1:JZhGawAyZ/EuJeBtbQYnaoftczcb2drR2Iq36Wgz4sQ= 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.1/go.mod h1:7bR2YD5euaxBhzt2y/oDkt3uNRb6tjFp98GlTFueRwk= 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.54 h1:4UmqeOqJPvdvASZWrKlhzpRahAulBfyTJQUaYy4+hEI= github.com/aws/aws-sdk-go-v2/credentials v1.17.58 h1:/d7FUpAPU8Lf2KUdjniQvfNdlMID0Sd9pS23FJ3SS9Y=
github.com/aws/aws-sdk-go-v2/credentials v1.17.54/go.mod h1:RTdfo0P0hbbTxIhmQrOsC/PquBZGabEPnCaxxKRPSnI= 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.24 h1:5grmdTdMsovn9kPZPI23Hhvp0ZyNm5cRO+IZFIYiAfw= 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.24/go.mod h1:zqi7TVKTswH3Ozq28PkmBmgzG1tona7mo9G2IJg4Cis= 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.52 h1:6kI83R98XOnnyzHv9g9KTYXFawMyeQq8NeEERWMAwJk= 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.52/go.mod h1:Juj7unpf3CIrWpEyJZhRJ6rJl9IYX7Hd8HOlwaZq/LE= 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.28 h1:igORFSiH3bfq4lxKFkTSYDhJEUCYo6C8VKiWJjYwQuQ= 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.28/go.mod h1:3So8EA/aAYm36L7XIvCVwLa0s5N0P7o2b1oqnx/2R4g= 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.28 h1:1mOW9zAUMhTSrMDssEHS/ajx8JcAj/IcftzcmNlmVLI= 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.28/go.mod h1:kGlXVIWDfvt2Ox5zEaNglmq0hXPHgQFNMix33Tw22jA= 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.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= 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.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= 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.28 h1:7kpeALOUeThs2kEjlAxlADAVfxKmkYAedlpZ3kdoSJ4= 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.28/go.mod h1:pyaOYEdp1MJWgtXLy6q80r3DhsVdOIOZNB9hdTcJIvI= 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 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/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 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/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/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.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA=
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/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY=
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.5 h1:siiQ+jummya9OLPDEyHVb2dLW4aOMe22FGDd0sAfuSw=
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/checksum v1.5.5/go.mod h1:iHVx2J9pWzITdP5MJY6qWfG34TfD9EA+Qi3eV6qQCXw=
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.12 h1:O+8vD2rGjfihBewr5bT+QUfYUHIxCVgG61LHoT59shM=
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/presigned-url v1.12.12/go.mod h1:usVdWJaosa66NMvmCrr08NcWDBRv4E6+YFG2pUdw1Lk=
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.12 h1:tkVNm99nkJnFo1H9IIQb5QkCiPcvCDn3Pos+IeTbGRA=
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/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 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/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.75.3 h1:JBod0SnNqcWQ0+uAyzeRFG1zCHotW8DukumYYyNy0zo=
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/s3 v1.75.3/go.mod h1:FHSHmyEUkzRbaFFqqm6bkLAOQHgqhsLmfCahvCBMiyA=
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.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.11/go.mod h1:Ro744S4fKiCCuZECXgOi760TiYylUM8ZBf6OGiZzJtY= 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.10 h1:l+dgv/64iVlQ3WsBbnn+JSbkj01jIi+SM0wYsj3y/hY= 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.10/go.mod h1:Fzsj6lZEb8AkTE5S68OhcbBqeWPsR8RnGuKPr8Todl8= 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.9 h1:BRVDbewN6VZcwr+FBOszDKvYeXY1kJ+GGMCcpghlw0U= 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.9/go.mod h1:f6vjfZER1M17Fokn0IzssOTMT2N8ZSq+7jnNF0tArvw= 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.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.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= 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 h1:bsVfwMh/emI6vreEveUEq9xAr6xtHLycTAGy2K7kvKM=
github.com/baidubce/bce-sdk-go v0.9.214/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= 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= 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.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/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.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/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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/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/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/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.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 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 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= 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/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= 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/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/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 h1:5LqzrJa8LADcY0sDEdV35e8nbwI7RoUQEt+KXWvWoY0=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.132/go.mod h1:JWz2ujO9X3oU5wb6kXp+DpR2UuDj2SldDbX8T0FSuhI= 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.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 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/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 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= 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= 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 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 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.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.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.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8/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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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/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.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.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= 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/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 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU=
github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs= 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.25.0 h1:/4YQq1hd0muvhzbERyUTVNh88N0BCj5diqK0jtLN6k8=
github.com/pocketbase/pocketbase v0.24.4/go.mod h1:EfXV/8RUY76jA6g1RPNHjOuW7wTd2bz0QlvAI/RU8YY= 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.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= 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= 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/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 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 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.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 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/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= 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-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-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-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-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= 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-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-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-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.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= 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-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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 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-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-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.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.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 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-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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/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-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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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-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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/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.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.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.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 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.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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 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.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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.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.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 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-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-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/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.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.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 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.219.0 h1:nnKIvxKs/06jWawp2liznTBnMRQBEPpGo7I+oEypTX0=
google.golang.org/api v0.217.0/go.mod h1:qMc2E8cBAbQlRypBTBWHklNJlaZZJBwDv81B1Iu8oSI= 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.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.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.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 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 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/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-20250204164813-702378808489 h1:5bKytslY8ViY0Cj/ewmRtrWHW64bNF03cAatUUFCdFI=
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/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 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.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 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.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= 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.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= 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-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-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 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.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.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.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.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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/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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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= 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 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= 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.15 h1:wFDan71KnYqeHz4eF63vmGE6Q6Pc0PUGDpP0PRMYjDc=
modernc.org/ccgo/v4 v4.23.13/go.mod h1:vdN4h2WR5aEoNondUx26K7G8X+nuBscYnAEWSRmN2/0= 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.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= 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.2 h1:YBXi5Kqp6aCK3fIxwKQ3/fErvawVKwjOLItxj1brGds=
modernc.org/gc/v2 v2.6.1/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.2/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.61.9 h1:PLSBXVkifXGELtJ5BOnBUyAHr7lsatNwFU/RRo4kfJM= modernc.org/libc v1.61.11 h1:6sZG8uB6EMMG7iTLPTndi8jyTdgAQNIeLGjCFICACZw=
modernc.org/libc v1.61.9/go.mod h1:61xrnzk/aR8gr5bR7Uj/lLFLuXu2/zMpIjcry63Eumk= 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 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= 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 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=

View File

@ -1,6 +1,7 @@
package applicant package applicant
import ( import (
"context"
"crypto" "crypto"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
@ -110,14 +111,11 @@ func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderCon
Kid: sslProviderConfig.Config.GoogleTrustServices.EabKid, Kid: sslProviderConfig.Config.GoogleTrustServices.EabKid,
HmacEncoded: sslProviderConfig.Config.GoogleTrustServices.EabHmacKey, HmacEncoded: sslProviderConfig.Config.GoogleTrustServices.EabHmacKey,
}) })
case sslProviderLetsEncrypt, sslProviderLetsEncryptStaging: case sslProviderLetsEncrypt, sslProviderLetsEncryptStaging:
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
default: default:
err = fmt.Errorf("unsupported ssl provider: %s", sslProviderConfig.Provider) err = fmt.Errorf("unsupported ssl provider: %s", sslProviderConfig.Provider)
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -129,7 +127,12 @@ func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderCon
return resp.Resource, nil 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) return nil, fmt.Errorf("failed to save registration: %w", err)
} }

View File

@ -26,6 +26,7 @@ type ApplyCertResult struct {
CertificateFullChain string CertificateFullChain string
IssuerCertificate string IssuerCertificate string
PrivateKey string PrivateKey string
ACMEAccountUrl string
ACMECertUrl string ACMECertUrl string
ACMECertStableUrl string ACMECertStableUrl string
CSR string CSR string
@ -46,8 +47,7 @@ type applicantOptions struct {
DnsPropagationTimeout int32 DnsPropagationTimeout int32
DnsTTL int32 DnsTTL int32
DisableFollowCNAME bool DisableFollowCNAME bool
DisableARI bool ReplacedARIAcctId string
SkipBeforeExpiryDays int32
ReplacedARICertId string ReplacedARICertId string
} }
@ -67,8 +67,6 @@ func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
DnsPropagationTimeout: nodeConfig.DnsPropagationTimeout, DnsPropagationTimeout: nodeConfig.DnsPropagationTimeout,
DnsTTL: nodeConfig.DnsTTL, DnsTTL: nodeConfig.DnsTTL,
DisableFollowCNAME: nodeConfig.DisableFollowCNAME, DisableFollowCNAME: nodeConfig.DisableFollowCNAME,
DisableARI: nodeConfig.DisableARI,
SkipBeforeExpiryDays: nodeConfig.SkipBeforeExpiryDays,
} }
accessRepo := repository.NewAccessRepository() accessRepo := repository.NewAccessRepository()
@ -95,6 +93,7 @@ func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
lastCertX509, _ := certcrypto.ParsePEMCertificate([]byte(lastCertificate.Certificate)) lastCertX509, _ := certcrypto.ParsePEMCertificate([]byte(lastCertificate.Certificate))
if lastCertX509 != nil { if lastCertX509 != nil {
replacedARICertId, _ := certificate.MakeARICertID(lastCertX509) replacedARICertId, _ := certificate.MakeARICertID(lastCertX509)
options.ReplacedARIAcctId = lastCertificate.ACMEAccountUrl
options.ReplacedARICertId = replacedARICertId options.ReplacedARICertId = replacedARICertId
} }
} }
@ -141,7 +140,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
// Create an ACME client config // Create an ACME client config
config := lego.NewConfig(acmeUser) config := lego.NewConfig(acmeUser)
config.CADirURL = sslProviderUrls[sslProviderConfig.Provider] config.CADirURL = sslProviderUrls[sslProviderConfig.Provider]
config.Certificate.KeyType = parseKeyAlgorithm(options.KeyAlgorithm) config.Certificate.KeyType = parseKeyAlgorithm(domain.CertificateKeyAlgorithmType(options.KeyAlgorithm))
// Create an ACME client // Create an ACME client
client, err := lego.NewClient(config) client, err := lego.NewClient(config)
@ -171,7 +170,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
Domains: options.Domains, Domains: options.Domains,
Bundle: true, Bundle: true,
} }
if !options.DisableARI { if options.ReplacedARICertId != "" && options.ReplacedARIAcctId != acmeUser.Registration.URI {
certRequest.ReplacesCertID = options.ReplacedARICertId certRequest.ReplacesCertID = options.ReplacedARICertId
} }
certResource, err := client.Certificate.Obtain(certRequest) certResource, err := client.Certificate.Obtain(certRequest)
@ -183,29 +182,30 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
CertificateFullChain: strings.TrimSpace(string(certResource.Certificate)), CertificateFullChain: strings.TrimSpace(string(certResource.Certificate)),
IssuerCertificate: strings.TrimSpace(string(certResource.IssuerCertificate)), IssuerCertificate: strings.TrimSpace(string(certResource.IssuerCertificate)),
PrivateKey: strings.TrimSpace(string(certResource.PrivateKey)), PrivateKey: strings.TrimSpace(string(certResource.PrivateKey)),
ACMEAccountUrl: acmeUser.Registration.URI,
ACMECertUrl: certResource.CertURL, ACMECertUrl: certResource.CertURL,
ACMECertStableUrl: certResource.CertStableURL, ACMECertStableUrl: certResource.CertStableURL,
CSR: strings.TrimSpace(string(certResource.CSR)), CSR: strings.TrimSpace(string(certResource.CSR)),
}, nil }, nil
} }
func parseKeyAlgorithm(algo string) certcrypto.KeyType { func parseKeyAlgorithm(algo domain.CertificateKeyAlgorithmType) certcrypto.KeyType {
switch algo { switch algo {
case "RSA2048": case domain.CertificateKeyAlgorithmTypeRSA2048:
return certcrypto.RSA2048 return certcrypto.RSA2048
case "RSA3072": case domain.CertificateKeyAlgorithmTypeRSA3072:
return certcrypto.RSA3072 return certcrypto.RSA3072
case "RSA4096": case domain.CertificateKeyAlgorithmTypeRSA4096:
return certcrypto.RSA4096 return certcrypto.RSA4096
case "RSA8192": case domain.CertificateKeyAlgorithmTypeRSA8192:
return certcrypto.RSA8192 return certcrypto.RSA8192
case "EC256": case domain.CertificateKeyAlgorithmTypeEC256:
return certcrypto.EC256 return certcrypto.EC256
case "EC384": case domain.CertificateKeyAlgorithmTypeEC384:
return certcrypto.EC384 return certcrypto.EC384
default:
return certcrypto.RSA2048
} }
return certcrypto.RSA2048
} }
// TODO: 暂时使用代理模式以兼容之前版本代码,后续重新实现此处逻辑 // TODO: 暂时使用代理模式以兼容之前版本代码,后续重新实现此处逻辑

View File

@ -35,8 +35,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
case domain.ApplyDNSProviderTypeACMEHttpReq: case domain.ApplyDNSProviderTypeACMEHttpReq:
{ {
access := domain.AccessConfigForACMEHttpReq{} access := domain.AccessConfigForACMEHttpReq{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
applicant, err := providerACMEHttpReq.NewChallengeProvider(&providerACMEHttpReq.ACMEHttpReqApplicantConfig{ applicant, err := providerACMEHttpReq.NewChallengeProvider(&providerACMEHttpReq.ACMEHttpReqApplicantConfig{
@ -52,8 +52,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
case domain.ApplyDNSProviderTypeAliyun, domain.ApplyDNSProviderTypeAliyunDNS: case domain.ApplyDNSProviderTypeAliyun, domain.ApplyDNSProviderTypeAliyunDNS:
{ {
access := domain.AccessConfigForAliyun{} access := domain.AccessConfigForAliyun{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
applicant, err := providerAliyun.NewChallengeProvider(&providerAliyun.AliyunApplicantConfig{ applicant, err := providerAliyun.NewChallengeProvider(&providerAliyun.AliyunApplicantConfig{
@ -68,8 +68,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
case domain.ApplyDNSProviderTypeAWS, domain.ApplyDNSProviderTypeAWSRoute53: case domain.ApplyDNSProviderTypeAWS, domain.ApplyDNSProviderTypeAWSRoute53:
{ {
access := domain.AccessConfigForAWS{} access := domain.AccessConfigForAWS{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
applicant, err := providerAWSRoute53.NewChallengeProvider(&providerAWSRoute53.AWSRoute53ApplicantConfig{ applicant, err := providerAWSRoute53.NewChallengeProvider(&providerAWSRoute53.AWSRoute53ApplicantConfig{
@ -86,8 +86,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
case domain.ApplyDNSProviderTypeAzureDNS: case domain.ApplyDNSProviderTypeAzureDNS:
{ {
access := domain.AccessConfigForAzure{} access := domain.AccessConfigForAzure{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
applicant, err := providerAzureDNS.NewChallengeProvider(&providerAzureDNS.AzureDNSApplicantConfig{ applicant, err := providerAzureDNS.NewChallengeProvider(&providerAzureDNS.AzureDNSApplicantConfig{
@ -104,8 +104,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
case domain.ApplyDNSProviderTypeCloudflare: case domain.ApplyDNSProviderTypeCloudflare:
{ {
access := domain.AccessConfigForCloudflare{} access := domain.AccessConfigForCloudflare{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
applicant, err := providerCloudflare.NewChallengeProvider(&providerCloudflare.CloudflareApplicantConfig{ applicant, err := providerCloudflare.NewChallengeProvider(&providerCloudflare.CloudflareApplicantConfig{
@ -119,8 +119,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
case domain.ApplyDNSProviderTypeClouDNS: case domain.ApplyDNSProviderTypeClouDNS:
{ {
access := domain.AccessConfigForClouDNS{} access := domain.AccessConfigForClouDNS{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
applicant, err := providerClouDNS.NewChallengeProvider(&providerClouDNS.ClouDNSApplicantConfig{ applicant, err := providerClouDNS.NewChallengeProvider(&providerClouDNS.ClouDNSApplicantConfig{
@ -135,8 +135,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
case domain.ApplyDNSProviderTypeGname: case domain.ApplyDNSProviderTypeGname:
{ {
access := domain.AccessConfigForGname{} access := domain.AccessConfigForGname{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
applicant, err := providerGname.NewChallengeProvider(&providerGname.GnameApplicantConfig{ applicant, err := providerGname.NewChallengeProvider(&providerGname.GnameApplicantConfig{
@ -151,8 +151,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
case domain.ApplyDNSProviderTypeGoDaddy: case domain.ApplyDNSProviderTypeGoDaddy:
{ {
access := domain.AccessConfigForGoDaddy{} access := domain.AccessConfigForGoDaddy{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
applicant, err := providerGoDaddy.NewChallengeProvider(&providerGoDaddy.GoDaddyApplicantConfig{ applicant, err := providerGoDaddy.NewChallengeProvider(&providerGoDaddy.GoDaddyApplicantConfig{
@ -167,8 +167,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
case domain.ApplyDNSProviderTypeHuaweiCloud, domain.ApplyDNSProviderTypeHuaweiCloudDNS: case domain.ApplyDNSProviderTypeHuaweiCloud, domain.ApplyDNSProviderTypeHuaweiCloudDNS:
{ {
access := domain.AccessConfigForHuaweiCloud{} access := domain.AccessConfigForHuaweiCloud{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
applicant, err := providerHuaweiCloud.NewChallengeProvider(&providerHuaweiCloud.HuaweiCloudApplicantConfig{ applicant, err := providerHuaweiCloud.NewChallengeProvider(&providerHuaweiCloud.HuaweiCloudApplicantConfig{
@ -184,8 +184,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
case domain.ApplyDNSProviderTypeNameDotCom: case domain.ApplyDNSProviderTypeNameDotCom:
{ {
access := domain.AccessConfigForNameDotCom{} access := domain.AccessConfigForNameDotCom{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
applicant, err := providerNameDotCom.NewChallengeProvider(&providerNameDotCom.NameDotComApplicantConfig{ applicant, err := providerNameDotCom.NewChallengeProvider(&providerNameDotCom.NameDotComApplicantConfig{
@ -200,8 +200,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
case domain.ApplyDNSProviderTypeNameSilo: case domain.ApplyDNSProviderTypeNameSilo:
{ {
access := domain.AccessConfigForNameSilo{} access := domain.AccessConfigForNameSilo{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
applicant, err := providerNameSilo.NewChallengeProvider(&providerNameSilo.NameSiloApplicantConfig{ applicant, err := providerNameSilo.NewChallengeProvider(&providerNameSilo.NameSiloApplicantConfig{
@ -215,8 +215,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
case domain.ApplyDNSProviderTypeNS1: case domain.ApplyDNSProviderTypeNS1:
{ {
access := domain.AccessConfigForNS1{} access := domain.AccessConfigForNS1{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
applicant, err := providerNS1.NewChallengeProvider(&providerNS1.NS1ApplicantConfig{ applicant, err := providerNS1.NewChallengeProvider(&providerNS1.NS1ApplicantConfig{
@ -230,8 +230,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
case domain.ApplyDNSProviderTypePowerDNS: case domain.ApplyDNSProviderTypePowerDNS:
{ {
access := domain.AccessConfigForPowerDNS{} access := domain.AccessConfigForPowerDNS{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
applicant, err := providerPowerDNS.NewChallengeProvider(&providerPowerDNS.PowerDNSApplicantConfig{ applicant, err := providerPowerDNS.NewChallengeProvider(&providerPowerDNS.PowerDNSApplicantConfig{
@ -246,8 +246,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
case domain.ApplyDNSProviderTypeRainYun: case domain.ApplyDNSProviderTypeRainYun:
{ {
access := domain.AccessConfigForRainYun{} access := domain.AccessConfigForRainYun{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
applicant, err := providerRainYun.NewChallengeProvider(&providerRainYun.RainYunApplicantConfig{ applicant, err := providerRainYun.NewChallengeProvider(&providerRainYun.RainYunApplicantConfig{
@ -261,8 +261,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
case domain.ApplyDNSProviderTypeTencentCloud, domain.ApplyDNSProviderTypeTencentCloudDNS: case domain.ApplyDNSProviderTypeTencentCloud, domain.ApplyDNSProviderTypeTencentCloudDNS:
{ {
access := domain.AccessConfigForTencentCloud{} access := domain.AccessConfigForTencentCloud{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
applicant, err := providerTencentCloud.NewChallengeProvider(&providerTencentCloud.TencentCloudApplicantConfig{ applicant, err := providerTencentCloud.NewChallengeProvider(&providerTencentCloud.TencentCloudApplicantConfig{
@ -277,8 +277,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
case domain.ApplyDNSProviderTypeVolcEngine, domain.ApplyDNSProviderTypeVolcEngineDNS: case domain.ApplyDNSProviderTypeVolcEngine, domain.ApplyDNSProviderTypeVolcEngineDNS:
{ {
access := domain.AccessConfigForVolcEngine{} access := domain.AccessConfigForVolcEngine{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
applicant, err := providerVolcEngine.NewChallengeProvider(&providerVolcEngine.VolcEngineApplicantConfig{ applicant, err := providerVolcEngine.NewChallengeProvider(&providerVolcEngine.VolcEngineApplicantConfig{
@ -293,8 +293,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
case domain.ApplyDNSProviderTypeWestcn: case domain.ApplyDNSProviderTypeWestcn:
{ {
access := domain.AccessConfigForWestcn{} access := domain.AccessConfigForWestcn{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
applicant, err := providerWestcn.NewChallengeProvider(&providerWestcn.WestcnApplicantConfig{ applicant, err := providerWestcn.NewChallengeProvider(&providerWestcn.WestcnApplicantConfig{

View File

@ -5,7 +5,7 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors" "fmt"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -30,18 +30,18 @@ type certificateRepository interface {
} }
type CertificateService struct { type CertificateService struct {
repo certificateRepository certRepo certificateRepository
} }
func NewCertificateService(repo certificateRepository) *CertificateService { func NewCertificateService(certRepo certificateRepository) *CertificateService {
return &CertificateService{ return &CertificateService{
repo: repo, certRepo: certRepo,
} }
} }
func (s *CertificateService) InitSchedule(ctx context.Context) error { func (s *CertificateService) InitSchedule(ctx context.Context) error {
app.GetScheduler().MustAdd("certificateExpireSoonNotify", "0 0 * * *", func() { app.GetScheduler().MustAdd("certificateExpireSoonNotify", "0 0 * * *", func() {
certs, err := s.repo.ListExpireSoon(context.Background()) certs, err := s.certRepo.ListExpireSoon(context.Background())
if err != nil { if err != nil {
app.GetLogger().Error("failed to get certificates which expire soon", "err", err) app.GetLogger().Error("failed to get certificates which expire soon", "err", err)
return return
@ -59,8 +59,8 @@ func (s *CertificateService) InitSchedule(ctx context.Context) error {
return nil return nil
} }
func (s *CertificateService) ArchiveFile(ctx context.Context, req *dtos.CertificateArchiveFileReq) ([]byte, error) { func (s *CertificateService) ArchiveFile(ctx context.Context, req *dtos.CertificateArchiveFileReq) (*dtos.CertificateArchiveFileResp, error) {
certificate, err := s.repo.GetById(ctx, req.CertificateId) certificate, err := s.certRepo.GetById(ctx, req.CertificateId)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -69,6 +69,10 @@ func (s *CertificateService) ArchiveFile(ctx context.Context, req *dtos.Certific
zipWriter := zip.NewWriter(&buf) zipWriter := zip.NewWriter(&buf)
defer zipWriter.Close() defer zipWriter.Close()
resp := &dtos.CertificateArchiveFileResp{
FileFormat: "zip",
}
switch strings.ToUpper(req.Format) { switch strings.ToUpper(req.Format) {
case "", "PEM": case "", "PEM":
{ {
@ -97,7 +101,8 @@ func (s *CertificateService) ArchiveFile(ctx context.Context, req *dtos.Certific
return nil, err return nil, err
} }
return buf.Bytes(), nil resp.FileBytes = buf.Bytes()
return resp, nil
} }
case "PFX": case "PFX":
@ -134,7 +139,8 @@ func (s *CertificateService) ArchiveFile(ctx context.Context, req *dtos.Certific
return nil, err return nil, err
} }
return buf.Bytes(), nil resp.FileBytes = buf.Bytes()
return resp, nil
} }
case "JKS": case "JKS":
@ -171,7 +177,8 @@ func (s *CertificateService) ArchiveFile(ctx context.Context, req *dtos.Certific
return nil, err return nil, err
} }
return buf.Bytes(), nil resp.FileBytes = buf.Bytes()
return resp, nil
} }
default: default:
@ -180,25 +187,30 @@ func (s *CertificateService) ArchiveFile(ctx context.Context, req *dtos.Certific
} }
func (s *CertificateService) ValidateCertificate(ctx context.Context, req *dtos.CertificateValidateCertificateReq) (*dtos.CertificateValidateCertificateResp, error) { func (s *CertificateService) ValidateCertificate(ctx context.Context, req *dtos.CertificateValidateCertificateReq) (*dtos.CertificateValidateCertificateResp, error) {
info, err := certs.ParseCertificateFromPEM(req.Certificate) certX509, err := certs.ParseCertificateFromPEM(req.Certificate)
if err != nil {
return nil, err
} else if time.Now().After(certX509.NotAfter) {
return nil, fmt.Errorf("certificate has expired at %s", certX509.NotAfter.UTC().Format(time.RFC3339))
}
return &dtos.CertificateValidateCertificateResp{
IsValid: true,
Domains: strings.Join(certX509.DNSNames, ";"),
}, nil
}
func (s *CertificateService) ValidatePrivateKey(ctx context.Context, req *dtos.CertificateValidatePrivateKeyReq) (*dtos.CertificateValidatePrivateKeyResp, error) {
_, err := certcrypto.ParsePEMPrivateKey([]byte(req.PrivateKey))
if err != nil { if err != nil {
return nil, err return nil, err
} }
if time.Now().After(info.NotAfter) { return &dtos.CertificateValidatePrivateKeyResp{
return nil, errors.New("证书已过期") IsValid: true,
}
return &dtos.CertificateValidateCertificateResp{
Domains: strings.Join(info.DNSNames, ";"),
}, nil }, nil
} }
func (s *CertificateService) ValidatePrivateKey(ctx context.Context, req *dtos.CertificateValidatePrivateKeyReq) error {
_, err := certcrypto.ParsePEMPrivateKey([]byte(req.PrivateKey))
return err
}
func buildExpireSoonNotification(certificates []*domain.Certificate) *struct { func buildExpireSoonNotification(certificates []*domain.Certificate) *struct {
Subject string Subject string
Message string Message string

View File

@ -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: case domain.DeployProviderTypeAliyunALB, domain.DeployProviderTypeAliyunCDN, domain.DeployProviderTypeAliyunCLB, domain.DeployProviderTypeAliyunDCDN, domain.DeployProviderTypeAliyunLive, domain.DeployProviderTypeAliyunNLB, domain.DeployProviderTypeAliyunOSS, domain.DeployProviderTypeAliyunWAF:
{ {
access := domain.AccessConfigForAliyun{} access := domain.AccessConfigForAliyun{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
switch options.Provider { switch options.Provider {
@ -146,8 +146,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeAWSCloudFront: case domain.DeployProviderTypeAWSCloudFront:
{ {
access := domain.AccessConfigForAWS{} access := domain.AccessConfigForAWS{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
switch options.Provider { switch options.Provider {
@ -168,8 +168,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeBaiduCloudCDN: case domain.DeployProviderTypeBaiduCloudCDN:
{ {
access := domain.AccessConfigForBaiduCloud{} access := domain.AccessConfigForBaiduCloud{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
switch options.Provider { switch options.Provider {
@ -189,8 +189,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeBytePlusCDN: case domain.DeployProviderTypeBytePlusCDN:
{ {
access := domain.AccessConfigForBytePlus{} access := domain.AccessConfigForBytePlus{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
switch options.Provider { switch options.Provider {
@ -210,8 +210,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeDogeCloudCDN: case domain.DeployProviderTypeDogeCloudCDN:
{ {
access := domain.AccessConfigForDogeCloud{} access := domain.AccessConfigForDogeCloud{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
deployer, err := providerDogeCDN.NewWithLogger(&providerDogeCDN.DogeCloudCDNDeployerConfig{ deployer, err := providerDogeCDN.NewWithLogger(&providerDogeCDN.DogeCloudCDNDeployerConfig{
@ -225,8 +225,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeEdgioApplications: case domain.DeployProviderTypeEdgioApplications:
{ {
access := domain.AccessConfigForEdgio{} access := domain.AccessConfigForEdgio{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
deployer, err := providerEdgioApplications.NewWithLogger(&providerEdgioApplications.EdgioApplicationsDeployerConfig{ deployer, err := providerEdgioApplications.NewWithLogger(&providerEdgioApplications.EdgioApplicationsDeployerConfig{
@ -240,8 +240,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeHuaweiCloudCDN, domain.DeployProviderTypeHuaweiCloudELB: case domain.DeployProviderTypeHuaweiCloudCDN, domain.DeployProviderTypeHuaweiCloudELB:
{ {
access := domain.AccessConfigForHuaweiCloud{} access := domain.AccessConfigForHuaweiCloud{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
switch options.Provider { switch options.Provider {
@ -291,8 +291,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeKubernetesSecret: case domain.DeployProviderTypeKubernetesSecret:
{ {
access := domain.AccessConfigForKubernetes{} access := domain.AccessConfigForKubernetes{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
deployer, err := providerK8sSecret.NewWithLogger(&providerK8sSecret.K8sSecretDeployerConfig{ deployer, err := providerK8sSecret.NewWithLogger(&providerK8sSecret.K8sSecretDeployerConfig{
@ -309,8 +309,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeQiniuCDN, domain.DeployProviderTypeQiniuPili: case domain.DeployProviderTypeQiniuCDN, domain.DeployProviderTypeQiniuPili:
{ {
access := domain.AccessConfigForQiniu{} access := domain.AccessConfigForQiniu{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
switch options.Provider { switch options.Provider {
@ -339,8 +339,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeSSH: case domain.DeployProviderTypeSSH:
{ {
access := domain.AccessConfigForSSH{} access := domain.AccessConfigForSSH{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
deployer, err := providerSSH.NewWithLogger(&providerSSH.SshDeployerConfig{ 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: case domain.DeployProviderTypeTencentCloudCDN, domain.DeployProviderTypeTencentCloudCLB, domain.DeployProviderTypeTencentCloudCOS, domain.DeployProviderTypeTencentCloudCSS, domain.DeployProviderTypeTencentCloudECDN, domain.DeployProviderTypeTencentCloudEO:
{ {
access := domain.AccessConfigForTencentCloud{} access := domain.AccessConfigForTencentCloud{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
switch options.Provider { switch options.Provider {
@ -435,8 +435,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeUCloudUCDN, domain.DeployProviderTypeUCloudUS3: case domain.DeployProviderTypeUCloudUCDN, domain.DeployProviderTypeUCloudUS3:
{ {
access := domain.AccessConfigForUCloud{} access := domain.AccessConfigForUCloud{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
switch options.Provider { 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: case domain.DeployProviderTypeVolcEngineCDN, domain.DeployProviderTypeVolcEngineCLB, domain.DeployProviderTypeVolcEngineDCDN, domain.DeployProviderTypeVolcEngineLive, domain.DeployProviderTypeVolcEngineTOS:
{ {
access := domain.AccessConfigForVolcEngine{} access := domain.AccessConfigForVolcEngine{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
switch options.Provider { switch options.Provider {
@ -525,8 +525,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeWebhook: case domain.DeployProviderTypeWebhook:
{ {
access := domain.AccessConfigForWebhook{} access := domain.AccessConfigForWebhook{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
} }
deployer, err := providerWebhook.NewWithLogger(&providerWebhook.WebhookDeployerConfig{ deployer, err := providerWebhook.NewWithLogger(&providerWebhook.WebhookDeployerConfig{

View File

@ -1,6 +1,12 @@
package domain package domain
import "time" import (
"crypto/x509"
"strings"
"time"
"github.com/usual2970/certimate/internal/pkg/utils/certs"
)
const CollectionNameCertificate = "certificate" const CollectionNameCertificate = "certificate"
@ -8,22 +14,81 @@ type Certificate struct {
Meta Meta
Source CertificateSourceType `json:"source" db:"source"` Source CertificateSourceType `json:"source" db:"source"`
SubjectAltNames string `json:"subjectAltNames" db:"subjectAltNames"` SubjectAltNames string `json:"subjectAltNames" db:"subjectAltNames"`
SerialNumber string `json:"serialNumber" db:"serialNumber"`
Certificate string `json:"certificate" db:"certificate"` Certificate string `json:"certificate" db:"certificate"`
PrivateKey string `json:"privateKey" db:"privateKey"` PrivateKey string `json:"privateKey" db:"privateKey"`
Issuer string `json:"issuer" db:"issuer"`
IssuerCertificate string `json:"issuerCertificate" db:"issuerCertificate"` IssuerCertificate string `json:"issuerCertificate" db:"issuerCertificate"`
KeyAlgorithm CertificateKeyAlgorithmType `json:"keyAlgorithm" db:"keyAlgorithm"`
EffectAt time.Time `json:"effectAt" db:"effectAt"` EffectAt time.Time `json:"effectAt" db:"effectAt"`
ExpireAt time.Time `json:"expireAt" db:"expireAt"` ExpireAt time.Time `json:"expireAt" db:"expireAt"`
ACMEAccountUrl string `json:"acmeAccountUrl" db:"acmeAccountUrl"`
ACMECertUrl string `json:"acmeCertUrl" db:"acmeCertUrl"` ACMECertUrl string `json:"acmeCertUrl" db:"acmeCertUrl"`
ACMECertStableUrl string `json:"acmeCertStableUrl" db:"acmeCertStableUrl"` ACMECertStableUrl string `json:"acmeCertStableUrl" db:"acmeCertStableUrl"`
WorkflowId string `json:"workflowId" db:"workflowId"` WorkflowId string `json:"workflowId" db:"workflowId"`
WorkflowNodeId string `json:"workflowNodeId" db:"workflowNodeId"` WorkflowNodeId string `json:"workflowNodeId" db:"workflowNodeId"`
WorkflowRunId string `json:"workflowRunId" db:"workflowRunId"`
WorkflowOutputId string `json:"workflowOutputId" db:"workflowOutputId"` WorkflowOutputId string `json:"workflowOutputId" db:"workflowOutputId"`
DeletedAt *time.Time `json:"deleted" db:"deleted"` 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 type CertificateSourceType string
const ( const (
CertificateSourceTypeWorkflow = CertificateSourceType("workflow") CertificateSourceTypeWorkflow = CertificateSourceType("workflow")
CertificateSourceTypeUpload = CertificateSourceType("upload") 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")
)

View File

@ -5,22 +5,24 @@ type CertificateArchiveFileReq struct {
Format string `json:"format"` Format string `json:"format"`
} }
type CertificateArchiveFileResp struct {
FileBytes []byte `json:"fileBytes"`
FileFormat string `json:"fileFormat"`
}
type CertificateValidateCertificateReq struct { type CertificateValidateCertificateReq struct {
Certificate string `json:"certificate"` Certificate string `json:"certificate"`
} }
type CertificateValidateCertificateResp struct { type CertificateValidateCertificateResp struct {
Domains string `json:"domains"` IsValid bool `json:"isValid"`
Domains string `json:"domains,omitempty"`
} }
type CertificateValidatePrivateKeyReq struct { type CertificateValidatePrivateKeyReq struct {
PrivateKey string `json:"privateKey"` PrivateKey string `json:"privateKey"`
} }
type CertificateUploadReq struct { type CertificateValidatePrivateKeyResp struct {
WorkflowId string `json:"workflowId"` IsValid bool `json:"isValid"`
WorkflowNodeId string `json:"workflowNodeId"`
CertificateId string `json:"certificateId"`
Certificate string `json:"certificate"`
PrivateKey string `json:"privateKey"`
} }

View File

@ -4,7 +4,7 @@ import "github.com/usual2970/certimate/internal/domain"
type WorkflowStartRunReq struct { type WorkflowStartRunReq struct {
WorkflowId string `json:"-"` WorkflowId string `json:"-"`
Trigger domain.WorkflowTriggerType `json:"trigger"` RunTrigger domain.WorkflowTriggerType `json:"trigger"`
} }
type WorkflowCancelRunReq struct { type WorkflowCancelRunReq struct {

View File

@ -55,8 +55,8 @@ type WorkflowNode struct {
Inputs []WorkflowNodeIO `json:"inputs"` Inputs []WorkflowNodeIO `json:"inputs"`
Outputs []WorkflowNodeIO `json:"outputs"` Outputs []WorkflowNodeIO `json:"outputs"`
Next *WorkflowNode `json:"next"` Next *WorkflowNode `json:"next,omitempty"`
Branches []WorkflowNode `json:"branches"` Branches []WorkflowNode `json:"branches,omitempty"`
Validated bool `json:"validated"` Validated bool `json:"validated"`
} }
@ -64,6 +64,7 @@ type WorkflowNode struct {
type WorkflowNodeConfigForApply struct { type WorkflowNodeConfigForApply struct {
Domains string `json:"domains"` // 域名列表,以半角逗号分隔 Domains string `json:"domains"` // 域名列表,以半角逗号分隔
ContactEmail string `json:"contactEmail"` // 联系邮箱 ContactEmail string `json:"contactEmail"` // 联系邮箱
ChallengeType string `json:"challengeType"` // TODO: 验证方式。目前仅支持 dns-01
Provider string `json:"provider"` // DNS 提供商 Provider string `json:"provider"` // DNS 提供商
ProviderAccessId string `json:"providerAccessId"` // DNS 提供商授权记录 ID ProviderAccessId string `json:"providerAccessId"` // DNS 提供商授权记录 ID
ProviderConfig map[string]any `json:"providerConfig"` // DNS 提供商额外配置 ProviderConfig map[string]any `json:"providerConfig"` // DNS 提供商额外配置

View File

@ -5,6 +5,7 @@ const CollectionNameWorkflowOutput = "workflow_output"
type WorkflowOutput struct { type WorkflowOutput struct {
Meta Meta
WorkflowId string `json:"workflowId" db:"workflow"` WorkflowId string `json:"workflowId" db:"workflow"`
RunId string `json:"runId" db:"runId"`
NodeId string `json:"nodeId" db:"nodeId"` NodeId string `json:"nodeId" db:"nodeId"`
Node *WorkflowNode `json:"node" db:"node"` Node *WorkflowNode `json:"node" db:"node"`
Outputs []WorkflowNodeIO `json:"outputs" db:"outputs"` Outputs []WorkflowNodeIO `json:"outputs" db:"outputs"`

View File

@ -31,17 +31,26 @@ const (
type WorkflowRunLog struct { type WorkflowRunLog struct {
NodeId string `json:"nodeId"` NodeId string `json:"nodeId"`
NodeName string `json:"nodeName"` NodeName string `json:"nodeName"`
Records []WorkflowRunLogRecord `json:"records"`
Error string `json:"error"` Error string `json:"error"`
Outputs []WorkflowRunLogOutput `json:"outputs"`
} }
type WorkflowRunLogOutput struct { type WorkflowRunLogRecord struct {
Time string `json:"time"` Time string `json:"time"`
Title string `json:"title"` Level WorkflowRunLogLevel `json:"level"`
Content string `json:"content"` Content string `json:"content"`
Error string `json:"error"` Error string `json:"error"`
} }
type WorkflowRunLogLevel string
const (
WorkflowRunLogLevelDebug WorkflowRunLogLevel = "DEBUG"
WorkflowRunLogLevelInfo WorkflowRunLogLevel = "INFO"
WorkflowRunLogLevelWarn WorkflowRunLogLevel = "WARN"
WorkflowRunLogLevelError WorkflowRunLogLevel = "ERROR"
)
type WorkflowRunLogs []WorkflowRunLog type WorkflowRunLogs []WorkflowRunLog
func (r WorkflowRunLogs) ErrorString() string { func (r WorkflowRunLogs) ErrorString() string {

View File

@ -173,8 +173,6 @@ func (d *DNSProvider) addOrUpdateDNSRecord(domain, subDomain, value string) erro
_, err := d.client.ModifyDomainResolution(request) _, err := d.client.ModifyDomainResolution(request)
return err return err
} }
return nil
} }
func (d *DNSProvider) removeDNSRecord(domain, subDomain, value string) error { func (d *DNSProvider) removeDNSRecord(domain, subDomain, value string) error {

View File

@ -2,13 +2,13 @@
import ( import (
"context" "context"
"encoding/pem"
"errors" "errors"
xerrors "github.com/pkg/errors" xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer" "github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/logger" "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" 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" 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) { func (d *EdgioApplicationsDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 提取 Edgio 所需的服务端证书和中间证书内容 // 提取 Edgio 所需的服务端证书和中间证书内容
privateCertPem, intermediateCertPem := extractCertChains(certPem) privateCertPem, intermediateCertPem, err := certs.ExtractCertificatesFromPEM(certPem)
if err != nil {
return nil, err
}
// 上传 TLS 证书 // 上传 TLS 证书
// REF: https://docs.edg.io/rest_api/#tag/tls-certs/operation/postConfigV01TlsCerts // 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, "", "") client := edgsdk.NewEdgioClient(clientId, clientSecret, "", "")
return client, nil 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
}

View File

@ -78,7 +78,7 @@ func (d *QiniuCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPe
// 获取域名信息 // 获取域名信息
// REF: https://developer.qiniu.com/fusion/4246/the-domain-name // 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 { if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetDomainInfo'") 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 // 判断域名是否已启用 HTTPS。如果已启用修改域名证书否则启用 HTTPS
// REF: https://developer.qiniu.com/fusion/4246/the-domain-name // REF: https://developer.qiniu.com/fusion/4246/the-domain-name
if getDomainInfoResp.Https != nil && getDomainInfoResp.Https.CertID != "" { 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 { if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.ModifyDomainHttpsConf'") return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.ModifyDomainHttpsConf'")
} }
d.logger.Logt("已修改域名证书", modifyDomainHttpsConfResp) d.logger.Logt("已修改域名证书", modifyDomainHttpsConfResp)
} else { } 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 { if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.EnableDomainHttps'") return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.EnableDomainHttps'")
} }

View File

@ -60,7 +60,7 @@ func (u *QiniuSSLCertUploader) Upload(ctx context.Context, certPem string, privk
// 上传新证书 // 上传新证书
// REF: https://developer.qiniu.com/fusion/8593/interface-related-certificate // 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 { if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.UploadSslCert'") return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.UploadSslCert'")
} }

View File

@ -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
}

View File

@ -13,6 +13,7 @@ import (
) )
// 从 PEM 编码的证书字符串解析并返回一个 x509.Certificate 对象。 // 从 PEM 编码的证书字符串解析并返回一个 x509.Certificate 对象。
// PEM 内容可能是包含多张证书的证书链,但只返回第一个证书(即服务器证书)。
// //
// 入参: // 入参:
// - certPem: 证书 PEM 内容。 // - certPem: 证书 PEM 内容。

View File

@ -183,7 +183,7 @@ func GetValueOrDefaultAsBool(dict map[string]any, key string, defaultValue bool)
return defaultValue return defaultValue
} }
// 将字典解码为指定类型的结构体。 // 将字典填充到指定类型的结构体。
// 与 [json.Unmarshal] 类似,但传入的是一个 [map[string]interface{}] 对象而非 JSON 格式的字符串。 // 与 [json.Unmarshal] 类似,但传入的是一个 [map[string]interface{}] 对象而非 JSON 格式的字符串。
// //
// 入参: // 入参:
@ -191,8 +191,8 @@ func GetValueOrDefaultAsBool(dict map[string]any, key string, defaultValue bool)
// - output: 结构体指针。 // - output: 结构体指针。
// //
// 出参: // 出参:
// - 错误信息。如果解码失败,则返回错误信息。 // - 错误信息。如果填充失败,则返回错误信息。
func Decode(dict map[string]any, output any) error { func Populate(dict map[string]any, output any) error {
config := &mapstructure.DecoderConfig{ config := &mapstructure.DecoderConfig{
Metadata: nil, Metadata: nil,
Result: output, Result: output,
@ -207,3 +207,8 @@ func Decode(dict map[string]any, output any) error {
return decoder.Decode(dict) return decoder.Decode(dict)
} }
// Deprecated: Use [Populate] instead.
func Decode(dict map[string]any, output any) error {
return Populate(dict, output)
}

View File

@ -150,7 +150,7 @@ func (c *GnameClient) sendRequestWithResult(path string, params map[string]any,
if err := json.Unmarshal(resp.Body(), &jsonResp); err != nil { if err := json.Unmarshal(resp.Body(), &jsonResp); err != nil {
return fmt.Errorf("failed to parse response: %w", err) 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) return fmt.Errorf("failed to parse response: %w", err)
} }

29
internal/pkg/vendors/qiniu-sdk/auth.go vendored Normal file
View File

@ -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)
}

View File

@ -1,48 +1,40 @@
package qiniusdk package qiniusdk
import ( import (
"bytes" "context"
"encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strings" "strings"
"github.com/qiniu/go-sdk/v7/auth" "github.com/qiniu/go-sdk/v7/auth"
"github.com/qiniu/go-sdk/v7/client"
) )
const qiniuHost = "https://api.qiniu.com" const qiniuHost = "https://api.qiniu.com"
type Client struct { type Client struct {
mac *auth.Credentials client *client.Client
} }
func NewClient(mac *auth.Credentials) *Client { func NewClient(mac *auth.Credentials) *Client {
if mac == nil { if mac == nil {
mac = auth.Default() 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) { func (c *Client) GetDomainInfo(ctx context.Context, domain string) (*GetDomainInfoResponse, error) {
respBytes, err := c.sendReq(http.MethodGet, fmt.Sprintf("domain/%s", domain), nil) resp := new(GetDomainInfoResponse)
if err != nil { if err := c.client.Call(ctx, resp, http.MethodGet, c.urlf("domain/%s", domain), nil); err != nil {
return nil, err 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 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{ req := &ModifyDomainHttpsConfRequest{
DomainInfoHttpsData: DomainInfoHttpsData{ DomainInfoHttpsData: DomainInfoHttpsData{
CertID: certId, CertID: certId,
@ -50,30 +42,14 @@ func (c *Client) ModifyDomainHttpsConf(domain, certId string, forceHttps, http2E
Http2Enable: http2Enable, Http2Enable: http2Enable,
}, },
} }
resp := new(ModifyDomainHttpsConfResponse)
reqBytes, err := json.Marshal(req) if err := c.client.CallWithJson(ctx, resp, http.MethodPut, c.urlf("domain/%s/httpsconf", domain), nil, req); err != nil {
if err != nil {
return nil, err 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 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{ req := &EnableDomainHttpsRequest{
DomainInfoHttpsData: DomainInfoHttpsData{ DomainInfoHttpsData: DomainInfoHttpsData{
CertID: certId, CertID: certId,
@ -81,83 +57,29 @@ func (c *Client) EnableDomainHttps(domain, certId string, forceHttps, http2Enabl
Http2Enable: http2Enable, Http2Enable: http2Enable,
}, },
} }
resp := new(EnableDomainHttpsResponse)
reqBytes, err := json.Marshal(req) if err := c.client.CallWithJson(ctx, resp, http.MethodPut, c.urlf("domain/%s/sslize", domain), nil, req); err != nil {
if err != nil {
return nil, err 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 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{ req := &UploadSslCertRequest{
Name: name, Name: name,
CommonName: commonName, CommonName: commonName,
Certificate: certificate, Certificate: certificate,
PrivateKey: privateKey, PrivateKey: privateKey,
} }
resp := new(UploadSslCertResponse)
reqBytes, err := json.Marshal(req) if err := c.client.CallWithJson(ctx, resp, http.MethodPost, c.urlf("sslcert"), nil, req); err != nil {
if err != nil {
return nil, err 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 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, "/") path = strings.TrimPrefix(path, "/")
return qiniuHost + "/" + 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
} }

View File

@ -13,7 +13,7 @@ type UploadSslCertRequest struct {
} }
type UploadSslCertResponse struct { type UploadSslCertResponse struct {
*BaseResponse BaseResponse
CertID string `json:"certID"` CertID string `json:"certID"`
} }

View File

@ -1,6 +1,9 @@
package repository package repository
import ( import (
"context"
"database/sql"
"errors"
"fmt" "fmt"
"github.com/go-acme/lego/v4/registration" "github.com/go-acme/lego/v4/registration"
@ -48,18 +51,37 @@ func (r *AcmeAccountRepository) GetByCAAndEmail(ca, email string) (*domain.AcmeA
return r.castRecordToModel(record) 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) collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameAcmeAccount)
if err != nil { if err != nil {
return err return acmeAccount, err
} }
record := core.NewRecord(collection) var record *core.Record
record.Set("ca", ca) if acmeAccount.Id == "" {
record.Set("email", email) record = core.NewRecord(collection)
record.Set("key", key) } else {
record.Set("resource", resource) record, err = app.GetApp().FindRecordById(collection, acmeAccount.Id)
return app.GetApp().Save(record) 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) { func (r *AcmeAccountRepository) castRecordToModel(record *core.Record) (*domain.AcmeAccount, error) {

View File

@ -79,6 +79,52 @@ func (r *CertificateRepository) GetByWorkflowNodeId(ctx context.Context, workflo
return r.castRecordToModel(records[0]) 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("workflowRunId", certificate.WorkflowRunId)
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) { func (r *CertificateRepository) castRecordToModel(record *core.Record) (*domain.Certificate, error) {
if record == nil { if record == nil {
return nil, fmt.Errorf("record is nil") return nil, fmt.Errorf("record is nil")
@ -92,14 +138,19 @@ func (r *CertificateRepository) castRecordToModel(record *core.Record) (*domain.
}, },
Source: domain.CertificateSourceType(record.GetString("source")), Source: domain.CertificateSourceType(record.GetString("source")),
SubjectAltNames: record.GetString("subjectAltNames"), SubjectAltNames: record.GetString("subjectAltNames"),
SerialNumber: record.GetString("serialNumber"),
Certificate: record.GetString("certificate"), Certificate: record.GetString("certificate"),
PrivateKey: record.GetString("privateKey"), PrivateKey: record.GetString("privateKey"),
Issuer: record.GetString("issuer"),
IssuerCertificate: record.GetString("issuerCertificate"), IssuerCertificate: record.GetString("issuerCertificate"),
KeyAlgorithm: domain.CertificateKeyAlgorithmType(record.GetString("keyAlgorithm")),
EffectAt: record.GetDateTime("effectAt").Time(), EffectAt: record.GetDateTime("effectAt").Time(),
ExpireAt: record.GetDateTime("expireAt").Time(), ExpireAt: record.GetDateTime("expireAt").Time(),
ACMEAccountUrl: record.GetString("acmeAccountUrl"),
ACMECertUrl: record.GetString("acmeCertUrl"), ACMECertUrl: record.GetString("acmeCertUrl"),
ACMECertStableUrl: record.GetString("acmeCertStableUrl"), ACMECertStableUrl: record.GetString("acmeCertStableUrl"),
WorkflowId: record.GetString("workflowId"), WorkflowId: record.GetString("workflowId"),
WorkflowRunId: record.GetString("workflowRunId"),
WorkflowNodeId: record.GetString("workflowNodeId"), WorkflowNodeId: record.GetString("workflowNodeId"),
WorkflowOutputId: record.GetString("workflowOutputId"), WorkflowOutputId: record.GetString("workflowOutputId"),
} }

View File

@ -24,7 +24,7 @@ func (r *WorkflowRepository) ListEnabledAuto(ctx context.Context) ([]*domain.Wor
"enabled={:enabled} && trigger={:trigger}", "enabled={:enabled} && trigger={:trigger}",
"-created", "-created",
0, 0, 0, 0,
dbx.Params{"enabled": true, "trigger": domain.WorkflowTriggerTypeAuto}, dbx.Params{"enabled": true, "trigger": string(domain.WorkflowTriggerTypeAuto)},
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -65,7 +65,7 @@ func (r *WorkflowRepository) Save(ctx context.Context, workflow *domain.Workflow
if workflow.Id == "" { if workflow.Id == "" {
record = core.NewRecord(collection) record = core.NewRecord(collection)
} else { } else {
record, err = app.GetApp().FindRecordById(domain.CollectionNameWorkflow, workflow.Id) record, err = app.GetApp().FindRecordById(collection, workflow.Id)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return workflow, domain.ErrRecordNotFound 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("lastRunId", workflow.LastRunId)
record.Set("lastRunStatus", string(workflow.LastRunStatus)) record.Set("lastRunStatus", string(workflow.LastRunStatus))
record.Set("lastRunTime", workflow.LastRunTime) record.Set("lastRunTime", workflow.LastRunTime)
if err := app.GetApp().Save(record); err != nil { if err := app.GetApp().Save(record); err != nil {
return workflow, err return workflow, err
} }
@ -96,65 +95,6 @@ func (r *WorkflowRepository) Save(ctx context.Context, workflow *domain.Workflow
return workflow, nil return workflow, nil
} }
func (r *WorkflowRepository) SaveRun(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error) {
collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflowRun)
if err != nil {
return workflowRun, err
}
var workflowRunRecord *core.Record
if workflowRun.Id == "" {
workflowRunRecord = core.NewRecord(collection)
} else {
workflowRunRecord, err = app.GetApp().FindRecordById(domain.CollectionNameWorkflowRun, workflowRun.Id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return workflowRun, err
}
workflowRunRecord = 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)
if err != nil {
return err
}
workflowRecord, err := txApp.FindRecordById(domain.CollectionNameWorkflow, workflowRun.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"))
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()
return nil
})
if err != nil {
return workflowRun, err
}
return workflowRun, nil
}
func (r *WorkflowRepository) castRecordToModel(record *core.Record) (*domain.Workflow, error) { func (r *WorkflowRepository) castRecordToModel(record *core.Record) (*domain.Workflow, error) {
if record == nil { if record == nil {
return nil, fmt.Errorf("record is nil") return nil, fmt.Errorf("record is nil")

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"errors" "errors"
"fmt"
"github.com/pocketbase/dbx" "github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/core"
@ -17,13 +18,13 @@ func NewWorkflowOutputRepository() *WorkflowOutputRepository {
return &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( records, err := app.GetApp().FindRecordsByFilter(
domain.CollectionNameWorkflowOutput, domain.CollectionNameWorkflowOutput,
"nodeId={:nodeId}", "nodeId={:nodeId}",
"-created", "-created",
1, 0, 1, 0,
dbx.Params{"nodeId": nodeId}, dbx.Params{"nodeId": workflowNodeId},
) )
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
@ -34,103 +35,128 @@ func (r *WorkflowOutputRepository) GetByNodeId(ctx context.Context, nodeId strin
if len(records) == 0 { if len(records) == 0 {
return nil, domain.ErrRecordNotFound 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(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(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 {
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
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{} node := &domain.WorkflowNode{}
if err := record.UnmarshalJSONField("node", node); err != nil { if err := record.UnmarshalJSONField("node", node); err != nil {
return nil, errors.New("failed to unmarshal node") return nil, err
} }
outputs := make([]domain.WorkflowNodeIO, 0) outputs := make([]domain.WorkflowNodeIO, 0)
if err := record.UnmarshalJSONField("outputs", &outputs); err != nil { if err := record.UnmarshalJSONField("outputs", &outputs); err != nil {
return nil, errors.New("failed to unmarshal output") return nil, err
} }
rs := &domain.WorkflowOutput{ workflowOutput := &domain.WorkflowOutput{
Meta: domain.Meta{ Meta: domain.Meta{
Id: record.Id, Id: record.Id,
CreatedAt: record.GetDateTime("created").Time(), CreatedAt: record.GetDateTime("created").Time(),
UpdatedAt: record.GetDateTime("updated").Time(), UpdatedAt: record.GetDateTime("updated").Time(),
}, },
WorkflowId: record.GetString("workflowId"), WorkflowId: record.GetString("workflowId"),
RunId: record.GetString("runId"),
NodeId: record.GetString("nodeId"), NodeId: record.GetString("nodeId"),
Node: node, Node: node,
Outputs: outputs, Outputs: outputs,
Succeeded: record.GetBool("succeeded"), Succeeded: record.GetBool("succeeded"),
} }
return workflowOutput, nil
return rs, nil
} }
// 保存节点输出 func (r *WorkflowOutputRepository) saveRecord(workflowOutput *domain.WorkflowOutput) (*core.Record, error) {
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
if output.Id == "" {
collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflowOutput) collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflowOutput)
if err != nil { if err != nil {
return err return nil, err
} }
var record *core.Record
if workflowOutput.Id == "" {
record = core.NewRecord(collection) record = core.NewRecord(collection)
} else { } else {
record, err = app.GetApp().FindRecordById(domain.CollectionNameWorkflowOutput, output.Id) record, err = app.GetApp().FindRecordById(collection, workflowOutput.Id)
if err != nil { if err != nil {
return err return record, err
} }
} }
record.Set("workflowId", output.WorkflowId) record.Set("workflowId", workflowOutput.WorkflowId)
record.Set("nodeId", output.NodeId) record.Set("runId", workflowOutput.RunId)
record.Set("node", output.Node) record.Set("nodeId", workflowOutput.NodeId)
record.Set("outputs", output.Outputs) record.Set("node", workflowOutput.Node)
record.Set("succeeded", output.Succeeded) record.Set("outputs", workflowOutput.Outputs)
record.Set("succeeded", workflowOutput.Succeeded)
if err := app.GetApp().Save(record); err != nil { if err := app.GetApp().Save(record); err != nil {
return err return record, err
} }
if cb != nil && certificate != nil { return record, 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
} }

View File

@ -0,0 +1,124 @@
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 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)
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
}

View File

@ -11,9 +11,9 @@ import (
) )
type certificateService interface { type certificateService interface {
ArchiveFile(ctx context.Context, req *dtos.CertificateArchiveFileReq) ([]byte, error) ArchiveFile(ctx context.Context, req *dtos.CertificateArchiveFileReq) (*dtos.CertificateArchiveFileResp, error)
ValidateCertificate(ctx context.Context, req *dtos.CertificateValidateCertificateReq) (*dtos.CertificateValidateCertificateResp, error) ValidateCertificate(ctx context.Context, req *dtos.CertificateValidateCertificateReq) (*dtos.CertificateValidateCertificateResp, error)
ValidatePrivateKey(ctx context.Context, req *dtos.CertificateValidatePrivateKeyReq) error ValidatePrivateKey(ctx context.Context, req *dtos.CertificateValidatePrivateKeyReq) (*dtos.CertificateValidatePrivateKeyResp, error)
} }
type CertificateHandler struct { type CertificateHandler struct {
@ -38,10 +38,10 @@ func (handler *CertificateHandler) archiveFile(e *core.RequestEvent) error {
return resp.Err(e, err) return resp.Err(e, err)
} }
if bt, err := handler.service.ArchiveFile(e.Request.Context(), req); err != nil { if res, err := handler.service.ArchiveFile(e.Request.Context(), req); err != nil {
return resp.Err(e, err) return resp.Err(e, err)
} else { } else {
return resp.Ok(e, bt) return resp.Ok(e, res)
} }
} }
@ -51,10 +51,10 @@ func (handler *CertificateHandler) validateCertificate(e *core.RequestEvent) err
return resp.Err(e, err) return resp.Err(e, err)
} }
if rs, err := handler.service.ValidateCertificate(e.Request.Context(), req); err != nil { if res, err := handler.service.ValidateCertificate(e.Request.Context(), req); err != nil {
return resp.Err(e, err) return resp.Err(e, err)
} else { } else {
return resp.Ok(e, rs) return resp.Ok(e, res)
} }
} }
@ -64,9 +64,9 @@ func (handler *CertificateHandler) validatePrivateKey(e *core.RequestEvent) erro
return resp.Err(e, err) return resp.Err(e, err)
} }
if err := handler.service.ValidatePrivateKey(e.Request.Context(), req); err != nil { if res, err := handler.service.ValidatePrivateKey(e.Request.Context(), req); err != nil {
return resp.Err(e, err) return resp.Err(e, err)
} else { } else {
return resp.Ok(e, nil) return resp.Ok(e, res)
} }
} }

View File

@ -13,7 +13,7 @@ import (
type workflowService interface { type workflowService interface {
StartRun(ctx context.Context, req *dtos.WorkflowStartRunReq) error StartRun(ctx context.Context, req *dtos.WorkflowStartRunReq) error
CancelRun(ctx context.Context, req *dtos.WorkflowCancelRunReq) error CancelRun(ctx context.Context, req *dtos.WorkflowCancelRunReq) error
Stop(ctx context.Context) Shutdown(ctx context.Context)
} }
type WorkflowHandler struct { type WorkflowHandler struct {

View File

@ -27,13 +27,14 @@ func Register(router *router.Router[*core.RequestEvent]) {
certificateSvc = certificate.NewCertificateService(certificateRepo) certificateSvc = certificate.NewCertificateService(certificateRepo)
workflowRepo := repository.NewWorkflowRepository() workflowRepo := repository.NewWorkflowRepository()
workflowSvc = workflow.NewWorkflowService(workflowRepo) workflowRunRepo := repository.NewWorkflowRunRepository()
workflowSvc = workflow.NewWorkflowService(workflowRepo, workflowRunRepo)
statisticsRepo := repository.NewStatisticsRepository() statisticsRepo := repository.NewStatisticsRepository()
statisticsSvc = statistics.NewStatisticsService(statisticsRepo) statisticsSvc = statistics.NewStatisticsService(statisticsRepo)
notifyRepo := repository.NewSettingsRepository() settingsRepo := repository.NewSettingsRepository()
notifySvc = notify.NewNotifyService(notifyRepo) notifySvc = notify.NewNotifyService(settingsRepo)
group := router.Group("/api") group := router.Group("/api")
group.Bind(apis.RequireSuperuserAuth()) group.Bind(apis.RequireSuperuserAuth())
@ -45,6 +46,6 @@ func Register(router *router.Router[*core.RequestEvent]) {
func Unregister() { func Unregister() {
if workflowSvc != nil { if workflowSvc != nil {
workflowSvc.Stop(context.Background()) workflowSvc.Shutdown(context.Background())
} }
} }

View File

@ -6,6 +6,6 @@ type certificateService interface {
InitSchedule(ctx context.Context) error InitSchedule(ctx context.Context) error
} }
func NewCertificateScheduler(service certificateService) error { func InitCertificateScheduler(service certificateService) error {
return service.InitSchedule(context.Background()) return service.InitSchedule(context.Background())
} }

View File

@ -1,6 +1,7 @@
package scheduler package scheduler
import ( import (
"github.com/usual2970/certimate/internal/app"
"github.com/usual2970/certimate/internal/certificate" "github.com/usual2970/certimate/internal/certificate"
"github.com/usual2970/certimate/internal/repository" "github.com/usual2970/certimate/internal/repository"
"github.com/usual2970/certimate/internal/workflow" "github.com/usual2970/certimate/internal/workflow"
@ -8,12 +9,17 @@ import (
func Register() { func Register() {
workflowRepo := repository.NewWorkflowRepository() workflowRepo := repository.NewWorkflowRepository()
workflowSvc := workflow.NewWorkflowService(workflowRepo) workflowRunRepo := repository.NewWorkflowRunRepository()
workflowSvc := workflow.NewWorkflowService(workflowRepo, workflowRunRepo)
certificateRepo := repository.NewCertificateRepository() certificateRepo := repository.NewCertificateRepository()
certificateSvc := certificate.NewCertificateService(certificateRepo) 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)
}
} }

View File

@ -6,6 +6,6 @@ type workflowService interface {
InitSchedule(ctx context.Context) error InitSchedule(ctx context.Context) error
} }
func NewWorkflowScheduler(service workflowService) error { func InitWorkflowScheduler(service workflowService) error {
return service.InitSchedule(context.Background()) return service.InitSchedule(context.Background())
} }

View File

@ -11,15 +11,15 @@ type statisticsRepository interface {
} }
type StatisticsService struct { type StatisticsService struct {
repo statisticsRepository statRepo statisticsRepository
} }
func NewStatisticsService(repo statisticsRepository) *StatisticsService { func NewStatisticsService(statRepo statisticsRepository) *StatisticsService {
return &StatisticsService{ return &StatisticsService{
repo: repo, statRepo: statRepo,
} }
} }
func (s *StatisticsService) Get(ctx context.Context) (*domain.Statistics, error) { func (s *StatisticsService) Get(ctx context.Context) (*domain.Statistics, error) {
return s.repo.Get(ctx) return s.statRepo.Get(ctx)
} }

View File

@ -0,0 +1,280 @@
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.workerMutex.Lock()
for _, worker := range w.workers {
worker.Cancel()
delete(w.workers, worker.Data.WorkflowId)
delete(w.workerIdMap, worker.Data.RunId)
}
w.workerMutex.Unlock()
w.wg.Wait()
}
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 := newWorkflowInvokerWithData(w.workflowRunRepo, 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)
}
}
}

View File

@ -0,0 +1,115 @@
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
workflowRunRepo workflowRunRepository
}
func newWorkflowInvokerWithData(workflowRunRepo workflowRunRepository, data *WorkflowWorkerData) *workflowInvoker {
if data == nil {
panic("worker data is nil")
}
// TODO: 待优化,日志与执行解耦
return &workflowInvoker{
workflowId: data.WorkflowId,
workflowContent: data.WorkflowContent,
runId: data.RunId,
runLogs: make([]domain.WorkflowRunLog, 0),
workflowRunRepo: workflowRunRepo,
}
}
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)
// TODO: 待优化,把 /pkg/core/* 包下的输出写入到 DEBUG 级别的日志中
if run, err := w.workflowRunRepo.GetById(ctx, w.runId); err == nil {
run.Logs = w.runLogs
w.workflowRunRepo.Save(ctx, run)
}
}
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
}

View File

@ -0,0 +1,32 @@
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 {
// TODO: 待优化构造过程
intanceOnce.Do(func() {
instance = newWorkflowDispatcher(workflowRepo, workflowRunRepo)
})
return instance
}

View File

@ -65,13 +65,13 @@ func onWorkflowRecordCreateOrUpdate(ctx context.Context, record *core.Record) er
// 反之,重新添加定时任务 // 反之,重新添加定时任务
err := scheduler.Add(fmt.Sprintf("workflow#%s", workflowId), record.GetString("triggerCron"), func() { 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, WorkflowId: workflowId,
Trigger: domain.WorkflowTriggerTypeAuto, RunTrigger: domain.WorkflowTriggerTypeAuto,
}) })
}) })
if err != nil { if err != nil {
app.GetLogger().Error("add cron job failed", "err", err)
return fmt.Errorf("add cron job failed: %w", err) return fmt.Errorf("add cron job failed: %w", err)
} }

View File

@ -3,7 +3,6 @@ package nodeprocessor
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"time" "time"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
@ -16,103 +15,92 @@ import (
type applyNode struct { type applyNode struct {
node *domain.WorkflowNode node *domain.WorkflowNode
*nodeLogger
certRepo certificateRepository certRepo certificateRepository
outputRepo workflowOutputRepository outputRepo workflowOutputRepository
*nodeLogger
} }
func NewApplyNode(node *domain.WorkflowNode) *applyNode { func NewApplyNode(node *domain.WorkflowNode) *applyNode {
return &applyNode{ return &applyNode{
node: node, node: node,
nodeLogger: NewNodeLogger(node), nodeLogger: newNodeLogger(node),
outputRepo: repository.NewWorkflowOutputRepository(),
certRepo: repository.NewCertificateRepository(), certRepo: repository.NewCertificateRepository(),
outputRepo: repository.NewWorkflowOutputRepository(),
} }
} }
// 申请节点根据申请类型执行不同的操作 func (n *applyNode) Process(ctx context.Context) error {
func (a *applyNode) Run(ctx context.Context) error { n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelInfo, "进入申请证书节点")
a.AddOutput(ctx, a.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) { if err != nil && !domain.IsRecordNotFoundError(err) {
a.AddOutput(ctx, a.node.Name, "查询申请记录失败", err.Error()) n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelError, "查询申请记录失败", err.Error())
return err return err
} }
// 检测是否可以跳过本次执行 // 检测是否可以跳过本次执行
if skippable, skipReason := a.checkCanSkip(ctx, lastOutput); skippable { if skippable, skipReason := n.checkCanSkip(ctx, lastOutput); skippable {
a.AddOutput(ctx, a.node.Name, skipReason) n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelInfo, skipReason)
return nil return nil
} }
// 初始化申请器 // 初始化申请器
applicant, err := applicant.NewWithApplyNode(a.node) applicant, err := applicant.NewWithApplyNode(n.node)
if err != nil { if err != nil {
a.AddOutput(ctx, a.node.Name, "获取申请对象失败", err.Error()) n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelError, "获取申请对象失败", err.Error())
return err return err
} }
// 申请证书 // 申请证书
applyResult, err := applicant.Apply() applyResult, err := applicant.Apply()
if err != nil { if err != nil {
a.AddOutput(ctx, a.node.Name, "申请失败", err.Error()) n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelError, "申请失败", err.Error())
return err return err
} }
a.AddOutput(ctx, a.node.Name, "申请成功") n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelInfo, "申请成功")
// 解析证书并生成实体 // 解析证书并生成实体
certX509, err := certs.ParseCertificateFromPEM(applyResult.CertificateFullChain) certX509, err := certs.ParseCertificateFromPEM(applyResult.CertificateFullChain)
if err != nil { if err != nil {
a.AddOutput(ctx, a.node.Name, "解析证书失败", err.Error()) n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelError, "解析证书失败", err.Error())
return err return err
} }
certificate := &domain.Certificate{ certificate := &domain.Certificate{
Source: domain.CertificateSourceTypeWorkflow, Source: domain.CertificateSourceTypeWorkflow,
SubjectAltNames: strings.Join(certX509.DNSNames, ";"),
Certificate: applyResult.CertificateFullChain, Certificate: applyResult.CertificateFullChain,
PrivateKey: applyResult.PrivateKey, PrivateKey: applyResult.PrivateKey,
IssuerCertificate: applyResult.IssuerCertificate, IssuerCertificate: applyResult.IssuerCertificate,
ACMEAccountUrl: applyResult.ACMEAccountUrl,
ACMECertUrl: applyResult.ACMECertUrl, ACMECertUrl: applyResult.ACMECertUrl,
ACMECertStableUrl: applyResult.ACMECertStableUrl, ACMECertStableUrl: applyResult.ACMECertStableUrl,
EffectAt: certX509.NotBefore,
ExpireAt: certX509.NotAfter,
WorkflowId: getContextWorkflowId(ctx),
WorkflowNodeId: a.node.Id,
} }
certificate.PopulateFromX509(certX509)
// 保存执行结果 // 保存执行结果
// TODO: 先保持一个节点始终只有一个输出,后续增加版本控制 output := &domain.WorkflowOutput{
currentOutput := &domain.WorkflowOutput{
WorkflowId: getContextWorkflowId(ctx), WorkflowId: getContextWorkflowId(ctx),
NodeId: a.node.Id, RunId: getContextWorkflowRunId(ctx),
Node: a.node, NodeId: n.node.Id,
Node: n.node,
Succeeded: true, Succeeded: true,
Outputs: a.node.Outputs, Outputs: n.node.Outputs,
} }
if lastOutput != nil { if _, err := n.outputRepo.SaveWithCertificate(ctx, output, certificate); err != nil {
currentOutput.Id = lastOutput.Id n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelError, "保存申请记录失败", err.Error())
}
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())
return err return err
} }
a.AddOutput(ctx, a.node.Name, "保存申请记录成功") n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelInfo, "保存申请记录成功")
return nil 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 { if lastOutput != nil && lastOutput.Succeeded {
// 比较和上次申请时的关键配置(即影响证书签发的)参数是否一致 // 比较和上次申请时的关键配置(即影响证书签发的)参数是否一致
currentNodeConfig := a.node.GetConfigForApply() currentNodeConfig := n.node.GetConfigForApply()
lastNodeConfig := lastOutput.Node.GetConfigForApply() lastNodeConfig := lastOutput.Node.GetConfigForApply()
if currentNodeConfig.Domains != lastNodeConfig.Domains { if currentNodeConfig.Domains != lastNodeConfig.Domains {
return false, "配置项变化:域名" return false, "配置项变化:域名"
@ -130,11 +118,13 @@ func (a *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo
return false, "配置项变化:数字签名算法" return false, "配置项变化:数字签名算法"
} }
lastCertificate, _ := a.certRepo.GetByWorkflowNodeId(ctx, a.node.Id) lastCertificate, _ := n.certRepo.GetByWorkflowNodeId(ctx, n.node.Id)
if lastCertificate != nil {
renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24 renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24
expirationTime := time.Until(lastCertificate.ExpireAt) expirationTime := time.Until(lastCertificate.ExpireAt)
if lastCertificate != nil && expirationTime > renewalInterval { 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)
}
} }
} }

View File

@ -14,15 +14,11 @@ type conditionNode struct {
func NewConditionNode(node *domain.WorkflowNode) *conditionNode { func NewConditionNode(node *domain.WorkflowNode) *conditionNode {
return &conditionNode{ return &conditionNode{
node: node, node: node,
nodeLogger: NewNodeLogger(node), nodeLogger: newNodeLogger(node),
} }
} }
// 条件节点没有任何操作 func (n *conditionNode) Process(ctx context.Context) error {
func (c *conditionNode) Run(ctx context.Context) error { // 此类型节点不需要执行任何操作,直接返回
c.AddOutput(ctx,
c.node.Name,
"完成",
)
return nil return nil
} }

View File

@ -13,95 +13,91 @@ import (
type deployNode struct { type deployNode struct {
node *domain.WorkflowNode node *domain.WorkflowNode
*nodeLogger
certRepo certificateRepository certRepo certificateRepository
outputRepo workflowOutputRepository outputRepo workflowOutputRepository
*nodeLogger
} }
func NewDeployNode(node *domain.WorkflowNode) *deployNode { func NewDeployNode(node *domain.WorkflowNode) *deployNode {
return &deployNode{ return &deployNode{
node: node, node: node,
nodeLogger: NewNodeLogger(node), nodeLogger: newNodeLogger(node),
outputRepo: repository.NewWorkflowOutputRepository(),
certRepo: repository.NewCertificateRepository(), certRepo: repository.NewCertificateRepository(),
outputRepo: repository.NewWorkflowOutputRepository(),
} }
} }
func (d *deployNode) Run(ctx context.Context) error { func (n *deployNode) Process(ctx context.Context) error {
d.AddOutput(ctx, d.node.Name, "开始执行") n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelInfo, "开始执行")
// 查询上次执行结果 // 查询上次执行结果
lastOutput, err := d.outputRepo.GetByNodeId(ctx, d.node.Id) lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id)
if err != nil && !domain.IsRecordNotFoundError(err) { if err != nil && !domain.IsRecordNotFoundError(err) {
d.AddOutput(ctx, d.node.Name, "查询部署记录失败", err.Error()) n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelError, "查询部署记录失败", err.Error())
return err return err
} }
// 获取前序节点输出证书 // 获取前序节点输出证书
previousNodeOutputCertificateSource := d.node.GetConfigForDeploy().Certificate previousNodeOutputCertificateSource := n.node.GetConfigForDeploy().Certificate
previousNodeOutputCertificateSourceSlice := strings.Split(previousNodeOutputCertificateSource, "#") previousNodeOutputCertificateSourceSlice := strings.Split(previousNodeOutputCertificateSource, "#")
if len(previousNodeOutputCertificateSourceSlice) != 2 { if len(previousNodeOutputCertificateSourceSlice) != 2 {
d.AddOutput(ctx, d.node.Name, "证书来源配置错误", previousNodeOutputCertificateSource) n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelError, "证书来源配置错误", previousNodeOutputCertificateSource)
return fmt.Errorf("证书来源配置错误: %s", 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 { if err != nil {
d.AddOutput(ctx, d.node.Name, "获取证书失败", err.Error()) n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelError, "获取证书失败", err.Error())
return err return err
} }
// 检测是否可以跳过本次执行 // 检测是否可以跳过本次执行
if skippable, skipReason := d.checkCanSkip(ctx, lastOutput); skippable { if lastOutput != nil && certificate.CreatedAt.Before(lastOutput.UpdatedAt) {
if certificate.CreatedAt.Before(lastOutput.UpdatedAt) { if skippable, skipReason := n.checkCanSkip(ctx, lastOutput); skippable {
d.AddOutput(ctx, d.node.Name, "已部署过且证书未更新") n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelInfo, skipReason)
} else {
d.AddOutput(ctx, d.node.Name, skipReason)
}
return nil return nil
} }
}
// 初始化部署器 // 初始化部署器
deploy, err := deployer.NewWithDeployNode(d.node, struct { deployer, err := deployer.NewWithDeployNode(n.node, struct {
Certificate string Certificate string
PrivateKey string PrivateKey string
}{Certificate: certificate.Certificate, PrivateKey: certificate.PrivateKey}) }{Certificate: certificate.Certificate, PrivateKey: certificate.PrivateKey})
if err != nil { if err != nil {
d.AddOutput(ctx, d.node.Name, "获取部署对象失败", err.Error()) n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelError, "获取部署对象失败", err.Error())
return err return err
} }
// 部署证书 // 部署证书
if err := deploy.Deploy(ctx); err != nil { if err := deployer.Deploy(ctx); err != nil {
d.AddOutput(ctx, d.node.Name, "部署失败", err.Error()) n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelError, "部署失败", err.Error())
return err return err
} }
d.AddOutput(ctx, d.node.Name, "部署成功") n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelInfo, "部署成功")
// 保存执行结果 // 保存执行结果
// TODO: 先保持一个节点始终只有一个输出,后续增加版本控制 output := &domain.WorkflowOutput{
currentOutput := &domain.WorkflowOutput{
Meta: domain.Meta{},
WorkflowId: getContextWorkflowId(ctx), WorkflowId: getContextWorkflowId(ctx),
NodeId: d.node.Id, RunId: getContextWorkflowRunId(ctx),
Node: d.node, NodeId: n.node.Id,
Node: n.node,
Succeeded: true, Succeeded: true,
} }
if lastOutput != nil { if _, err := n.outputRepo.Save(ctx, output); err != nil {
currentOutput.Id = lastOutput.Id n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelError, "保存部署记录失败", err.Error())
}
if err := d.outputRepo.Save(ctx, currentOutput, nil, nil); err != nil {
d.AddOutput(ctx, d.node.Name, "保存部署记录失败", err.Error())
return err return err
} }
d.AddOutput(ctx, d.node.Name, "保存部署记录成功") n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelInfo, "保存部署记录成功")
return nil 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 { if lastOutput != nil && lastOutput.Succeeded {
// 比较和上次部署时的关键配置(即影响证书部署的)参数是否一致 // 比较和上次部署时的关键配置(即影响证书部署的)参数是否一致
currentNodeConfig := d.node.GetConfigForDeploy() currentNodeConfig := n.node.GetConfigForDeploy()
lastNodeConfig := lastOutput.Node.GetConfigForDeploy() lastNodeConfig := lastOutput.Node.GetConfigForDeploy()
if currentNodeConfig.ProviderAccessId != lastNodeConfig.ProviderAccessId { if currentNodeConfig.ProviderAccessId != lastNodeConfig.ProviderAccessId {
return false, "配置项变化:主机提供商授权" return false, "配置项变化:主机提供商授权"

View File

@ -14,14 +14,13 @@ type executeFailureNode struct {
func NewExecuteFailureNode(node *domain.WorkflowNode) *executeFailureNode { func NewExecuteFailureNode(node *domain.WorkflowNode) *executeFailureNode {
return &executeFailureNode{ return &executeFailureNode{
node: node, node: node,
nodeLogger: NewNodeLogger(node), nodeLogger: newNodeLogger(node),
} }
} }
func (e *executeFailureNode) Run(ctx context.Context) error { func (n *executeFailureNode) Process(ctx context.Context) error {
e.AddOutput(ctx, // 此类型节点不需要执行任何操作,直接返回
e.node.Name, n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelInfo, "进入执行失败分支")
"进入执行失败分支",
)
return nil return nil
} }

View File

@ -14,14 +14,13 @@ type executeSuccessNode struct {
func NewExecuteSuccessNode(node *domain.WorkflowNode) *executeSuccessNode { func NewExecuteSuccessNode(node *domain.WorkflowNode) *executeSuccessNode {
return &executeSuccessNode{ return &executeSuccessNode{
node: node, node: node,
nodeLogger: NewNodeLogger(node), nodeLogger: newNodeLogger(node),
} }
} }
func (e *executeSuccessNode) Run(ctx context.Context) error { func (n *executeSuccessNode) Process(ctx context.Context) error {
e.AddOutput(ctx, // 此类型节点不需要执行任何操作,直接返回
e.node.Name, n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelInfo, "进入执行成功分支")
"进入执行成功分支",
)
return nil return nil
} }

View File

@ -10,43 +10,45 @@ import (
type notifyNode struct { type notifyNode struct {
node *domain.WorkflowNode node *domain.WorkflowNode
settingsRepo settingsRepository
*nodeLogger *nodeLogger
settingsRepo settingsRepository
} }
func NewNotifyNode(node *domain.WorkflowNode) *notifyNode { func NewNotifyNode(node *domain.WorkflowNode) *notifyNode {
return &notifyNode{ return &notifyNode{
node: node, node: node,
nodeLogger: NewNodeLogger(node), nodeLogger: newNodeLogger(node),
settingsRepo: repository.NewSettingsRepository(), settingsRepo: repository.NewSettingsRepository(),
} }
} }
func (n *notifyNode) Run(ctx context.Context) error { func (n *notifyNode) Process(ctx context.Context) error {
n.AddOutput(ctx, n.node.Name, "开始执行") n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelInfo, "进入推送通知节点")
nodeConfig := n.node.GetConfigForNotify() nodeConfig := n.node.GetConfigForNotify()
// 获取通知配置 // 获取通知配置
settings, err := n.settingsRepo.GetByName(ctx, "notifyChannels") settings, err := n.settingsRepo.GetByName(ctx, "notifyChannels")
if err != nil { if err != nil {
n.AddOutput(ctx, n.node.Name, "获取通知配置失败", err.Error()) n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelError, "获取通知配置失败", err.Error())
return err return err
} }
// 获取通知渠道 // 获取通知渠道
channelConfig, err := settings.GetNotifyChannelConfig(nodeConfig.Channel) channelConfig, err := settings.GetNotifyChannelConfig(nodeConfig.Channel)
if err != nil { if err != nil {
n.AddOutput(ctx, n.node.Name, "获取通知渠道配置失败", err.Error()) n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelError, "获取通知渠道配置失败", err.Error())
return err return err
} }
// 发送通知 // 发送通知
if err := notify.SendToChannel(nodeConfig.Subject, nodeConfig.Message, nodeConfig.Channel, channelConfig); err != nil { if err := notify.SendToChannel(nodeConfig.Subject, nodeConfig.Message, nodeConfig.Channel, channelConfig); err != nil {
n.AddOutput(ctx, n.node.Name, "发送通知失败", err.Error()) n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelError, "发送通知失败", err.Error())
return err return err
} }
n.AddOutput(ctx, n.node.Name, "发送通知成功") n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelInfo, "发送通知成功")
return nil return nil
} }

View File

@ -9,9 +9,10 @@ import (
) )
type NodeProcessor interface { type NodeProcessor interface {
Run(ctx context.Context) error Process(ctx context.Context) error
Log(ctx context.Context) *domain.WorkflowRunLog
AddOutput(ctx context.Context, title, content string, err ...string) GetLog(ctx context.Context) *domain.WorkflowRunLog
AppendLogRecord(ctx context.Context, level domain.WorkflowRunLogLevel, content string, err ...string)
} }
type nodeLogger struct { type nodeLogger struct {
@ -23,39 +24,41 @@ type certificateRepository interface {
} }
type workflowOutputRepository interface { type workflowOutputRepository interface {
GetByNodeId(ctx context.Context, nodeId string) (*domain.WorkflowOutput, error) GetByNodeId(ctx context.Context, workflowNodeId string) (*domain.WorkflowOutput, error)
Save(ctx context.Context, output *domain.WorkflowOutput, certificate *domain.Certificate, cb func(id string) error) 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 { type settingsRepository interface {
GetByName(ctx context.Context, name string) (*domain.Settings, error) GetByName(ctx context.Context, name string) (*domain.Settings, error)
} }
func NewNodeLogger(node *domain.WorkflowNode) *nodeLogger { func newNodeLogger(node *domain.WorkflowNode) *nodeLogger {
return &nodeLogger{ return &nodeLogger{
log: &domain.WorkflowRunLog{ log: &domain.WorkflowRunLog{
NodeId: node.Id, NodeId: node.Id,
NodeName: node.Name, NodeName: node.Name,
Outputs: make([]domain.WorkflowRunLogOutput, 0), Records: make([]domain.WorkflowRunLogRecord, 0),
}, },
} }
} }
func (l *nodeLogger) Log(ctx context.Context) *domain.WorkflowRunLog { func (l *nodeLogger) GetLog(ctx context.Context) *domain.WorkflowRunLog {
return l.log return l.log
} }
func (l *nodeLogger) AddOutput(ctx context.Context, title, content string, err ...string) { func (l *nodeLogger) AppendLogRecord(ctx context.Context, level domain.WorkflowRunLogLevel, content string, err ...string) {
output := domain.WorkflowRunLogOutput{ record := domain.WorkflowRunLogRecord{
Time: time.Now().UTC().Format(time.RFC3339), Time: time.Now().UTC().Format(time.RFC3339),
Title: title, Level: level,
Content: content, Content: content,
} }
if len(err) > 0 { if len(err) > 0 {
output.Error = err[0] record.Error = err[0]
l.log.Error = err[0] l.log.Error = err[0]
} }
l.log.Outputs = append(l.log.Outputs, output)
l.log.Records = append(l.log.Records, record)
} }
func GetProcessor(node *domain.WorkflowNode) (NodeProcessor, error) { func GetProcessor(node *domain.WorkflowNode) (NodeProcessor, error) {
@ -83,3 +86,7 @@ func GetProcessor(node *domain.WorkflowNode) (NodeProcessor, error) {
func getContextWorkflowId(ctx context.Context) string { func getContextWorkflowId(ctx context.Context) string {
return ctx.Value("workflow_id").(string) return ctx.Value("workflow_id").(string)
} }
func getContextWorkflowRunId(ctx context.Context) string {
return ctx.Value("workflow_run_id").(string)
}

View File

@ -14,13 +14,13 @@ type startNode struct {
func NewStartNode(node *domain.WorkflowNode) *startNode { func NewStartNode(node *domain.WorkflowNode) *startNode {
return &startNode{ return &startNode{
node: node, node: node,
nodeLogger: NewNodeLogger(node), nodeLogger: newNodeLogger(node),
} }
} }
func (s *startNode) Run(ctx context.Context) error { func (n *startNode) Process(ctx context.Context) error {
// 开始节点没有任何操作 // 此类型节点不需要执行任何操作,直接返回
s.AddOutput(ctx, s.node.Name, "完成") n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelInfo, "进入开始节点")
return nil return nil
} }

View File

@ -13,89 +13,93 @@ import (
type uploadNode struct { type uploadNode struct {
node *domain.WorkflowNode node *domain.WorkflowNode
outputRepo workflowOutputRepository
*nodeLogger *nodeLogger
certRepo certificateRepository
outputRepo workflowOutputRepository
} }
func NewUploadNode(node *domain.WorkflowNode) *uploadNode { func NewUploadNode(node *domain.WorkflowNode) *uploadNode {
return &uploadNode{ return &uploadNode{
node: node, node: node,
nodeLogger: NewNodeLogger(node), nodeLogger: newNodeLogger(node),
certRepo: repository.NewCertificateRepository(),
outputRepo: repository.NewWorkflowOutputRepository(), outputRepo: repository.NewWorkflowOutputRepository(),
} }
} }
// Run 上传证书节点执行 func (n *uploadNode) Process(ctx context.Context) error {
// 包含上传证书的工作流,理论上应该手动执行,如果每天定时执行,也只是重新保存一下 n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelInfo, "进入上传证书节点")
func (n *uploadNode) Run(ctx context.Context) error {
n.AddOutput(ctx,
n.node.Name,
"进入上传证书节点",
)
config := n.node.GetConfigForUpload() nodeConfig := n.node.GetConfigForUpload()
// 检查证书是否过期 // 查询上次执行结果
// 如果证书过期,则直接返回错误 lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id)
certX509, err := certs.ParseCertificateFromPEM(config.Certificate) if err != nil && !domain.IsRecordNotFoundError(err) {
if err != nil { n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelError, "查询申请记录失败", err.Error())
n.AddOutput(ctx,
n.node.Name,
"解析证书失败",
)
return err return err
} }
// 检测是否可以跳过本次执行
if skippable, skipReason := n.checkCanSkip(ctx, lastOutput); skippable {
n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelInfo, skipReason)
return nil
}
// 检查证书是否过期
// 如果证书过期,则直接返回错误
certX509, err := certs.ParseCertificateFromPEM(nodeConfig.Certificate)
if err != nil {
n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelError, "解析证书失败")
return err
}
if time.Now().After(certX509.NotAfter) { if time.Now().After(certX509.NotAfter) {
n.AddOutput(ctx, n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelWarn, "证书已过期")
n.node.Name,
"证书已过期",
)
return errors.New("certificate is expired") return errors.New("certificate is expired")
} }
// 生成证书实体
certificate := &domain.Certificate{ certificate := &domain.Certificate{
Source: domain.CertificateSourceTypeUpload, 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,
} }
certificate.PopulateFromPEM(nodeConfig.Certificate, nodeConfig.PrivateKey)
// 保存执行结果 // 保存执行结果
// TODO: 先保持一个节点始终只有一个输出,后续增加版本控制 output := &domain.WorkflowOutput{
currentOutput := &domain.WorkflowOutput{
WorkflowId: getContextWorkflowId(ctx), WorkflowId: getContextWorkflowId(ctx),
RunId: getContextWorkflowRunId(ctx),
NodeId: n.node.Id, NodeId: n.node.Id,
Node: n.node, Node: n.node,
Succeeded: true, Succeeded: true,
Outputs: n.node.Outputs, Outputs: n.node.Outputs,
} }
if _, err := n.outputRepo.SaveWithCertificate(ctx, output, certificate); err != nil {
// 查询上次执行结果 n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelError, "保存上传记录失败", err.Error())
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 return err
} }
if lastOutput != nil { n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelInfo, "保存上传记录成功")
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 {
n.AddOutput(ctx, n.node.Name, "保存上传记录失败", err.Error())
return err
}
n.AddOutput(ctx, n.node.Name, "保存上传记录成功")
return nil 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, ""
}

View File

@ -1,89 +0,0 @@
package processor
import (
"context"
"github.com/usual2970/certimate/internal/domain"
nodes "github.com/usual2970/certimate/internal/workflow/node-processor"
)
type workflowProcessor struct {
workflow *domain.Workflow
logs []domain.WorkflowRunLog
}
func NewWorkflowProcessor(workflow *domain.Workflow) *workflowProcessor {
return &workflowProcessor{
workflow: workflow,
logs: make([]domain.WorkflowRunLog, 0),
}
}
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 {
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.Run(ctx)
log := processor.Log(ctx)
if log != nil {
w.logs = append(w.logs, *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 = 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)
} else {
current = current.Next
}
}
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 {
return &branch
}
}
return nil
}

View File

@ -4,70 +4,64 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"sync"
"time" "time"
"github.com/usual2970/certimate/internal/app" "github.com/usual2970/certimate/internal/app"
"github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/domain/dtos" "github.com/usual2970/certimate/internal/domain/dtos"
processor "github.com/usual2970/certimate/internal/workflow/processor" "github.com/usual2970/certimate/internal/workflow/dispatcher"
) )
const defaultRoutines = 10
type workflowRunData struct {
Workflow *domain.Workflow
RunTrigger domain.WorkflowTriggerType
}
type workflowRepository interface { type workflowRepository interface {
ListEnabledAuto(ctx context.Context) ([]*domain.Workflow, error) ListEnabledAuto(ctx context.Context) ([]*domain.Workflow, error)
GetById(ctx context.Context, id string) (*domain.Workflow, error) GetById(ctx context.Context, id string) (*domain.Workflow, error)
Save(ctx context.Context, workflow *domain.Workflow) (*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 { type WorkflowService struct {
ch chan *workflowRunData dispatcher *dispatcher.WorkflowDispatcher
repo workflowRepository
wg sync.WaitGroup workflowRepo workflowRepository
cancel context.CancelFunc workflowRunRepo workflowRunRepository
} }
func NewWorkflowService(repo workflowRepository) *WorkflowService { func NewWorkflowService(workflowRepo workflowRepository, workflowRunRepo workflowRunRepository) *WorkflowService {
srv := &WorkflowService{ srv := &WorkflowService{
repo: repo, dispatcher: dispatcher.GetSingletonDispatcher(workflowRepo, workflowRunRepo),
ch: make(chan *workflowRunData, 1),
workflowRepo: workflowRepo,
workflowRunRepo: workflowRunRepo,
} }
ctx, cancel := context.WithCancel(context.Background())
srv.cancel = cancel
srv.wg.Add(defaultRoutines)
for i := 0; i < defaultRoutines; i++ {
go srv.run(ctx)
}
return srv return srv
} }
func (s *WorkflowService) InitSchedule(ctx context.Context) error { func (s *WorkflowService) InitSchedule(ctx context.Context) error {
workflows, err := s.repo.ListEnabledAuto(ctx) workflows, err := s.workflowRepo.ListEnabledAuto(ctx)
if err != nil { if err != nil {
return err return err
} }
scheduler := app.GetScheduler() scheduler := app.GetScheduler()
for _, workflow := range workflows { for _, workflow := range workflows {
var errs []error
err := scheduler.Add(fmt.Sprintf("workflow#%s", workflow.Id), workflow.TriggerCron, func() { err := scheduler.Add(fmt.Sprintf("workflow#%s", workflow.Id), workflow.TriggerCron, func() {
s.StartRun(ctx, &dtos.WorkflowStartRunReq{ s.StartRun(ctx, &dtos.WorkflowStartRunReq{
WorkflowId: workflow.Id, WorkflowId: workflow.Id,
Trigger: domain.WorkflowTriggerTypeAuto, RunTrigger: domain.WorkflowTriggerTypeAuto,
}) })
}) })
if err != nil { if err != nil {
app.GetLogger().Error("failed to add schedule", "err", err) errs = append(errs, err)
return err }
if len(errs) > 0 {
return errors.Join(errs...)
} }
} }
@ -75,97 +69,56 @@ func (s *WorkflowService) InitSchedule(ctx context.Context) error {
} }
func (s *WorkflowService) StartRun(ctx context.Context, req *dtos.WorkflowStartRunReq) 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 { if err != nil {
app.GetLogger().Error("failed to get workflow", "id", req.WorkflowId, "err", err)
return err return err
} }
if workflow.LastRunStatus == domain.WorkflowRunStatusTypeRunning { if workflow.LastRunStatus == domain.WorkflowRunStatusTypePending || workflow.LastRunStatus == domain.WorkflowRunStatusTypeRunning {
return errors.New("workflow is running") return errors.New("workflow is already pending or 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
}
s.ch <- &workflowRunData{
Workflow: workflow,
RunTrigger: req.Trigger,
}
return nil
}
func (s *WorkflowService) CancelRun(ctx context.Context, req *dtos.WorkflowCancelRunReq) error {
// TODO: 取消运行,防止因为某些原因意外挂起(如进程被杀死)导致工作流一直处于 running 状态无法重新运行
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{ run := &domain.WorkflowRun{
WorkflowId: workflow.Id, WorkflowId: workflow.Id,
Status: domain.WorkflowRunStatusTypeRunning, Status: domain.WorkflowRunStatusTypePending,
Trigger: runData.RunTrigger, Trigger: req.RunTrigger,
StartedAt: time.Now(), 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 return err
} else { } else {
run = resp run = resp
} }
processor := processor.NewWorkflowProcessor(workflow) s.dispatcher.Dispatch(&dispatcher.WorkflowWorkerData{
if runErr := processor.Run(ctx); runErr != nil { WorkflowId: workflow.Id,
run.Status = domain.WorkflowRunStatusTypeFailed WorkflowContent: workflow.Content,
run.EndedAt = time.Now() RunId: run.Id,
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 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()
}

View File

@ -18,7 +18,8 @@ import (
"github.com/usual2970/certimate/internal/scheduler" "github.com/usual2970/certimate/internal/scheduler"
"github.com/usual2970/certimate/internal/workflow" "github.com/usual2970/certimate/internal/workflow"
"github.com/usual2970/certimate/ui" "github.com/usual2970/certimate/ui"
//_ "github.com/usual2970/certimate/migrations"
_ "github.com/usual2970/certimate/migrations"
) )
func main() { func main() {

View File

@ -12,16 +12,16 @@ func init() {
return err return err
} }
record, _ := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "admin@certimate.fun")
if record == nil {
record := core.NewRecord(superusers) record := core.NewRecord(superusers)
record.Set("email", "admin@certimate.fun") record.Set("email", "admin@certimate.fun")
record.Set("password", "1234567890") record.Set("password", "1234567890")
return app.Save(record) return app.Save(record)
}, func(app core.App) error {
record, _ := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "admin@certimate.fun")
if record == nil {
return nil
} }
return app.Delete(record) return nil
}, func(app core.App) error {
return nil
}) })
} }

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -0,0 +1,58 @@
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(1, []byte(`{
"cascadeDelete": true,
"collectionId": "tovyif5ax6j62ur",
"hidden": false,
"id": "m8xfsyyy",
"maxSelect": 1,
"minSelect": 0,
"name": "workflowId",
"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("qjp8lygssgwyqyz")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
"cascadeDelete": false,
"collectionId": "tovyif5ax6j62ur",
"hidden": false,
"id": "m8xfsyyy",
"maxSelect": 1,
"minSelect": 0,
"name": "workflowId",
"presentable": false,
"required": false,
"system": false,
"type": "relation"
}`)); err != nil {
return err
}
return app.Save(collection)
})
}

View File

@ -0,0 +1,92 @@
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("bqnxb95f2cooowp")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
"cascadeDelete": true,
"collectionId": "tovyif5ax6j62ur",
"hidden": false,
"id": "jka88auc",
"maxSelect": 1,
"minSelect": 0,
"name": "workflowId",
"presentable": false,
"required": false,
"system": false,
"type": "relation"
}`)); err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{
"cascadeDelete": true,
"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 field
if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{
"cascadeDelete": false,
"collectionId": "tovyif5ax6j62ur",
"hidden": false,
"id": "jka88auc",
"maxSelect": 1,
"minSelect": 0,
"name": "workflowId",
"presentable": false,
"required": false,
"system": false,
"type": "relation"
}`)); err != nil {
return err
}
// update 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)
})
}

View File

@ -3,10 +3,14 @@ import { ClientResponseError } from "pocketbase";
import { type CertificateFormatType } from "@/domain/certificate"; import { type CertificateFormatType } from "@/domain/certificate";
import { getPocketBase } from "@/repository/_pocketbase"; import { getPocketBase } from "@/repository/_pocketbase";
type ArchiveRespData = {
fileBytes: string;
};
export const archive = async (certificateId: string, format?: CertificateFormatType) => { export const archive = async (certificateId: string, format?: CertificateFormatType) => {
const pb = getPocketBase(); const pb = getPocketBase();
const resp = await pb.send<BaseResponse<string>>(`/api/certificates/${encodeURIComponent(certificateId)}/archive`, { const resp = await pb.send<BaseResponse<ArchiveRespData>>(`/api/certificates/${encodeURIComponent(certificateId)}/archive`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -24,6 +28,7 @@ export const archive = async (certificateId: string, format?: CertificateFormatT
}; };
type ValidateCertificateResp = { type ValidateCertificateResp = {
isValid: boolean;
domains: string; domains: string;
}; };
@ -46,9 +51,13 @@ export const validateCertificate = async (certificate: string) => {
return resp; return resp;
}; };
type ValidatePrivateKeyResp = {
isValid: boolean;
};
export const validatePrivateKey = async (privateKey: string) => { export const validatePrivateKey = async (privateKey: string) => {
const pb = getPocketBase(); const pb = getPocketBase();
const resp = await pb.send<BaseResponse>(`/api/certificates/validate/private-key`, { const resp = await pb.send<BaseResponse<ValidatePrivateKeyResp>>(`/api/certificates/validate/private-key`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",

View File

@ -22,7 +22,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
const handleDownloadClick = async (format: CertificateFormatType) => { const handleDownloadClick = async (format: CertificateFormatType) => {
try { try {
const res = await archiveCertificate(data.id, format); const res = await archiveCertificate(data.id, format);
const bstr = atob(res.data); const bstr = atob(res.data.fileBytes);
const u8arr = Uint8Array.from(bstr, (ch) => ch.charCodeAt(0)); const u8arr = Uint8Array.from(bstr, (ch) => ch.charCodeAt(0));
const blob = new Blob([u8arr], { type: "application/zip" }); const blob = new Blob([u8arr], { type: "application/zip" });
saveAs(blob, `${data.id}-${data.subjectAltNames}.zip`); saveAs(blob, `${data.id}-${data.subjectAltNames}.zip`);
@ -38,11 +38,27 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
<Form layout="vertical"> <Form layout="vertical">
<Form.Item label={t("certificate.props.subject_alt_names")}> <Form.Item label={t("certificate.props.subject_alt_names")}>
<Input value={data.subjectAltNames} placeholder="" /> <Input value={data.subjectAltNames} variant="filled" placeholder="" />
</Form.Item>
<Form.Item label={t("certificate.props.issuer")}>
<Input value={data.issuer} variant="filled" placeholder="" />
</Form.Item> </Form.Item>
<Form.Item label={t("certificate.props.validity")}> <Form.Item label={t("certificate.props.validity")}>
<Input value={`${dayjs(data.effectAt).format("YYYY-MM-DD HH:mm:ss")} ~ ${dayjs(data.expireAt).format("YYYY-MM-DD HH:mm:ss")}`} placeholder="" /> <Input
value={`${dayjs(data.effectAt).format("YYYY-MM-DD HH:mm:ss")} ~ ${dayjs(data.expireAt).format("YYYY-MM-DD HH:mm:ss")}`}
variant="filled"
placeholder=""
/>
</Form.Item>
<Form.Item label={t("certificate.props.serial_number")}>
<Input value={data.serialNumber} variant="filled" placeholder="" />
</Form.Item>
<Form.Item label={t("certificate.props.key_algorithm")}>
<Input value={data.keyAlgorithm} variant="filled" placeholder="" />
</Form.Item> </Form.Item>
<Form.Item> <Form.Item>
@ -59,7 +75,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
</CopyToClipboard> </CopyToClipboard>
</Tooltip> </Tooltip>
</div> </div>
<Input.TextArea value={data.certificate} rows={10} autoSize={{ maxRows: 10 }} readOnly /> <Input.TextArea value={data.certificate} variant="filled" rows={5} autoSize={{ maxRows: 5 }} readOnly />
</Form.Item> </Form.Item>
<Form.Item> <Form.Item>
@ -76,7 +92,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
</CopyToClipboard> </CopyToClipboard>
</Tooltip> </Tooltip>
</div> </div>
<Input.TextArea value={data.privateKey} rows={10} autoSize={{ maxRows: 10 }} readOnly /> <Input.TextArea value={data.privateKey} variant="filled" rows={5} autoSize={{ maxRows: 5 }} readOnly />
</Form.Item> </Form.Item>
</Form> </Form>

View File

@ -0,0 +1,163 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
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;
style?: React.CSSProperties;
data: WorkflowRunModel;
};
const WorkflowRunDetail = ({ data, ...props }: WorkflowRunDetailProps) => {
const { t } = useTranslation();
return (
<div {...props}>
<Show when={data.status === WORKFLOW_RUN_STATUSES.SUCCEEDED}>
<Alert showIcon type="success" message={<Typography.Text type="success">{t("workflow_run.props.status.succeeded")}</Typography.Text>} />
</Show>
<Show when={data.status === WORKFLOW_RUN_STATUSES.FAILED}>
<Alert showIcon type="error" message={<Typography.Text type="danger">{t("workflow_run.props.status.failed")}</Typography.Text>} />
</Show>
<div className="my-4">
<Typography.Title level={5}>{t("workflow_run.logs")}</Typography.Title>
<div className="rounded-md bg-black p-4 text-stone-200">
<div className="flex flex-col space-y-4">
{data.logs?.map((item, i) => {
return (
<div key={i} className="flex flex-col space-y-2">
<div className="font-semibold">{item.nodeName}</div>
<div className="flex flex-col space-y-1">
{item.records?.map((output, j) => {
return (
<div key={j} className="flex space-x-2 text-sm" style={{ wordBreak: "break-word" }}>
<div className="whitespace-nowrap">[{dayjs(output.time).format("YYYY-MM-DD HH:mm:ss")}]</div>
{output.error ? <div className="text-red-500">{output.error}</div> : <div>{output.content}</div>}
</div>
);
})}
</div>
</div>
);
})}
</div>
</div>
</div>
<Show when={data.status === WORKFLOW_RUN_STATUSES.SUCCEEDED}>
<Divider />
<WorkflowRunArtifacts runId={data.id} />
</Show>
</div>
);
};
const WorkflowRunArtifacts = ({ runId }: { runId: string }) => {
const { t } = useTranslation();
const [notificationApi, NotificationContextHolder] = notification.useNotification();
const tableColumns: TableProps<CertificateModel>["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 (
<Typography.Text delete={!!record.deleted} ellipsis>
{record.subjectAltNames}
</Typography.Text>
);
},
},
{
key: "$action",
align: "end",
width: 120,
render: (_, record) => (
<Button.Group>
<CertificateDetailDrawer
data={record}
trigger={
<Tooltip title={t("certificate.action.view")}>
<Button color="primary" disabled={!!record.deleted} icon={<SelectOutlinedIcon />} variant="text" />
</Tooltip>
}
/>
</Button.Group>
),
},
];
const [tableData, setTableData] = useState<CertificateModel[]>([]);
const { loading: tableLoading } = useRequest(
() => {
return listCertificateByWorkflowRunId(runId);
},
{
refreshDeps: [runId],
onBefore: () => {
setTableData([]);
},
onSuccess: (res) => {
setTableData(res.items);
},
onError: (err) => {
if (err instanceof ClientResponseError && err.isAbort) {
return;
}
console.error(err);
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
throw err;
},
}
);
return (
<>
{NotificationContextHolder}
<Typography.Title level={5}>{t("workflow_run.artifacts")}</Typography.Title>
<Table<CertificateModel>
columns={tableColumns}
dataSource={tableData}
loading={tableLoading}
locale={{
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />,
}}
pagination={false}
rowKey={(record) => record.id}
size="small"
/>
</>
);
};
export default WorkflowRunDetail;

View File

@ -1,12 +1,12 @@
import { useTranslation } from "react-i18next";
import { useControllableValue } from "ahooks"; import { useControllableValue } from "ahooks";
import { Alert, Drawer, Typography } from "antd"; import { Drawer } from "antd";
import dayjs from "dayjs";
import Show from "@/components/Show"; 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 { useTriggerElement } from "@/hooks";
import WorkflowRunDetail from "./WorkflowRunDetail";
export type WorkflowRunDetailDrawerProps = { export type WorkflowRunDetailDrawerProps = {
data?: WorkflowRunModel; data?: WorkflowRunModel;
loading?: boolean; loading?: boolean;
@ -16,8 +16,6 @@ export type WorkflowRunDetailDrawerProps = {
}; };
const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowRunDetailDrawerProps) => { const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowRunDetailDrawerProps) => {
const { t } = useTranslation();
const [open, setOpen] = useControllableValue<boolean>(props, { const [open, setOpen] = useControllableValue<boolean>(props, {
valuePropName: "open", valuePropName: "open",
defaultValuePropName: "defaultOpen", defaultValuePropName: "defaultOpen",
@ -30,37 +28,19 @@ const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowR
<> <>
{triggerEl} {triggerEl}
<Drawer destroyOnClose open={open} loading={loading} placement="right" title={`WorkflowRun #${data?.id}`} width={640} onClose={() => setOpen(false)}> <Drawer
afterOpenChange={setOpen}
closable
destroyOnClose
open={open}
loading={loading}
placement="right"
title={`WorkflowRun #${data?.id}`}
width={640}
onClose={() => setOpen(false)}
>
<Show when={!!data}> <Show when={!!data}>
<Show when={data!.status === WORKFLOW_RUN_STATUSES.SUCCEEDED}> <WorkflowRunDetail data={data!} />
<Alert showIcon type="success" message={<Typography.Text type="success">{t("workflow_run.props.status.succeeded")}</Typography.Text>} />
</Show>
<Show when={data!.status === WORKFLOW_RUN_STATUSES.FAILED}>
<Alert showIcon type="error" message={<Typography.Text type="danger">{t("workflow_run.props.status.failed")}</Typography.Text>} />
</Show>
<div className="mt-4 rounded-md bg-black p-4 text-stone-200">
<div className="flex flex-col space-y-4">
{data!.logs?.map((item, i) => {
return (
<div key={i} className="flex flex-col space-y-2">
<div className="font-semibold">{item.nodeName}</div>
<div className="flex flex-col space-y-1">
{item.outputs?.map((output, j) => {
return (
<div key={j} className="flex space-x-2 text-sm" style={{ wordBreak: "break-word" }}>
<div className="whitespace-nowrap">[{dayjs(output.time).format("YYYY-MM-DD HH:mm:ss")}]</div>
{output.error ? <div className="text-red-500">{output.error}</div> : <div>{output.content}</div>}
</div>
);
})}
</div>
</div>
);
})}
</div>
</div>
</Show> </Show>
</Drawer> </Drawer>
</> </>

View File

@ -1,13 +1,13 @@
import { useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
CheckCircleOutlined as CheckCircleOutlinedIcon, CheckCircleOutlined as CheckCircleOutlinedIcon,
ClockCircleOutlined as ClockCircleOutlinedIcon, ClockCircleOutlined as ClockCircleOutlinedIcon,
CloseCircleOutlined as CloseCircleOutlinedIcon, CloseCircleOutlined as CloseCircleOutlinedIcon,
DeleteOutlined as DeleteOutlinedIcon, DeleteOutlined as DeleteOutlinedIcon,
PauseCircleOutlined as PauseCircleOutlinedIcon,
PauseOutlined as PauseOutlinedIcon, PauseOutlined as PauseOutlinedIcon,
SelectOutlined as SelectOutlinedIcon, SelectOutlined as SelectOutlinedIcon,
StopOutlined as StopOutlinedIcon,
SyncOutlined as SyncOutlinedIcon, SyncOutlined as SyncOutlinedIcon,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { useRequest } from "ahooks"; import { useRequest } from "ahooks";
@ -18,7 +18,12 @@ import { ClientResponseError } from "pocketbase";
import { cancelRun as cancelWorkflowRun } from "@/api/workflows"; import { cancelRun as cancelWorkflowRun } from "@/api/workflows";
import { WORKFLOW_TRIGGERS } from "@/domain/workflow"; import { WORKFLOW_TRIGGERS } from "@/domain/workflow";
import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun"; 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 { getErrMsg } from "@/utils/error";
import WorkflowRunDetailDrawer from "./WorkflowRunDetailDrawer"; import WorkflowRunDetailDrawer from "./WorkflowRunDetailDrawer";
@ -75,7 +80,7 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
); );
} else if (record.status === WORKFLOW_RUN_STATUSES.CANCELED) { } else if (record.status === WORKFLOW_RUN_STATUSES.CANCELED) {
return ( return (
<Tag icon={<PauseCircleOutlinedIcon />} color="warning"> <Tag icon={<StopOutlinedIcon />} color="warning">
{t("workflow_run.props.status.canceled")} {t("workflow_run.props.status.canceled")}
</Tag> </Tag>
); );
@ -211,6 +216,31 @@ 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];
});
if (cb.record.status !== WORKFLOW_RUN_STATUSES.PENDING && cb.record.status !== WORKFLOW_RUN_STATUSES.RUNNING) {
unsubscribeWorkflowRun(item.id);
}
});
}
return () => {
for (const item of items) {
unsubscribeWorkflowRun(item.id);
}
};
}, [tableData]);
const handleCancelClick = (workflowRun: WorkflowRunModel) => { const handleCancelClick = (workflowRun: WorkflowRunModel) => {
modalApi.confirm({ modalApi.confirm({
title: t("workflow_run.action.cancel"), title: t("workflow_run.action.cancel"),
@ -275,7 +305,7 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
setPageSize(pageSize); setPageSize(pageSize);
}, },
}} }}
rowKey={(record: WorkflowRunModel) => record.id} rowKey={(record) => record.id}
scroll={{ x: "max(100%, 960px)" }} scroll={{ x: "max(100%, 960px)" }}
/> />
</div> </div>

View File

@ -56,6 +56,7 @@ const ApplyNode = ({ node, disabled }: ApplyNodeProps) => {
const newNode = produce(node, (draft) => { const newNode = produce(node, (draft) => {
draft.config = { draft.config = {
...newValues, ...newValues,
challengeType: newValues.challengeType || "dns-01", // 默认使用 DNS-01 认证
}; };
draft.validated = true; draft.validated = true;
}); });

View File

@ -56,6 +56,7 @@ const MULTIPLE_INPUT_DELIMITER = ";";
const initFormModel = (): ApplyNodeConfigFormFieldValues => { const initFormModel = (): ApplyNodeConfigFormFieldValues => {
return { return {
challengeType: "dns-01",
keyAlgorithm: "RSA2048", keyAlgorithm: "RSA2048",
skipBeforeExpiryDays: 20, skipBeforeExpiryDays: 20,
}; };
@ -74,6 +75,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
.every((e) => validDomainName(e, { allowWildcard: true })); .every((e) => validDomainName(e, { allowWildcard: true }));
}, t("common.errmsg.domain_invalid")), }, t("common.errmsg.domain_invalid")),
contactEmail: z.string({ message: t("workflow_node.apply.form.contact_email.placeholder") }).email(t("common.errmsg.email_invalid")), contactEmail: z.string({ message: t("workflow_node.apply.form.contact_email.placeholder") }).email(t("common.errmsg.email_invalid")),
challengeType: z.string().nullish(),
provider: z.string({ message: t("workflow_node.apply.form.provider.placeholder") }).nonempty(t("workflow_node.apply.form.provider.placeholder")), provider: z.string({ message: t("workflow_node.apply.form.provider.placeholder") }).nonempty(t("workflow_node.apply.form.provider.placeholder")),
providerAccessId: z providerAccessId: z
.string({ message: t("workflow_node.apply.form.provider_access.placeholder") }) .string({ message: t("workflow_node.apply.form.provider_access.placeholder") })
@ -235,6 +237,16 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
<EmailInput placeholder={t("workflow_node.apply.form.contact_email.placeholder")} /> <EmailInput placeholder={t("workflow_node.apply.form.contact_email.placeholder")} />
</Form.Item> </Form.Item>
<Form.Item name="challengeType" label={t("workflow_node.apply.form.challenge_type.label")} rules={[formRule]} hidden>
<Select
options={["DNS-01"].map((e) => ({
label: e,
value: e.toLowerCase(),
}))}
placeholder={t("workflow_node.apply.form.challenge_type.placeholder")}
/>
</Form.Item>
<Form.Item name="provider" label={t("workflow_node.apply.form.provider.label")} hidden rules={[formRule]}> <Form.Item name="provider" label={t("workflow_node.apply.form.provider.label")} hidden rules={[formRule]}>
<ApplyDNSProviderSelect <ApplyDNSProviderSelect
allowClear allowClear

View File

@ -86,7 +86,7 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
</Form.Item> </Form.Item>
<Form.Item name="message" label={t("workflow_node.notify.form.message.label")} rules={[formRule]}> <Form.Item name="message" label={t("workflow_node.notify.form.message.label")} rules={[formRule]}>
<Input.TextArea autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t("workflow_node.notify.form.message.placeholder")} /> <Input.TextArea autoSize={{ minRows: 3, maxRows: 5 }} placeholder={t("workflow_node.notify.form.message.placeholder")} />
</Form.Item> </Form.Item>
<Form.Item className="mb-0"> <Form.Item className="mb-0">

View File

@ -3,8 +3,7 @@ import { useTranslation } from "react-i18next";
import { Flex, Typography } from "antd"; import { Flex, Typography } from "antd";
import { produce } from "immer"; import { produce } from "immer";
import type { WorkflowNodeConfigForUpload } from "@/domain/workflow"; import { type WorkflowNodeConfigForUpload, WorkflowNodeType } from "@/domain/workflow";
import { WorkflowNodeType } from "@/domain/workflow";
import { useZustandShallowSelector } from "@/hooks"; import { useZustandShallowSelector } from "@/hooks";
import { useWorkflowStore } from "@/stores/workflow"; import { useWorkflowStore } from "@/stores/workflow";

View File

@ -137,11 +137,11 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
return ( return (
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}> <Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
<Form.Item name="domains" label={t("workflow_node.upload.form.domains.label")} rules={[formRule]}> <Form.Item name="domains" label={t("workflow_node.upload.form.domains.label")} rules={[formRule]}>
<Input placeholder={t("workflow_node.upload.form.domains.placeholder")} readOnly /> <Input variant="filled" placeholder={t("workflow_node.upload.form.domains.placeholder")} readOnly />
</Form.Item> </Form.Item>
<Form.Item name="certificate" label={t("workflow_node.upload.form.certificate.label")} rules={[formRule]}> <Form.Item name="certificate" label={t("workflow_node.upload.form.certificate.label")} rules={[formRule]}>
<Input.TextArea readOnly autoSize={{ minRows: 5, maxRows: 10 }} placeholder={t("workflow_node.upload.form.certificate.placeholder")} /> <Input.TextArea readOnly autoSize={{ minRows: 5, maxRows: 5 }} placeholder={t("workflow_node.upload.form.certificate.placeholder")} />
</Form.Item> </Form.Item>
<Form.Item> <Form.Item>
@ -151,7 +151,7 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
</Form.Item> </Form.Item>
<Form.Item name="privateKey" label={t("workflow_node.upload.form.private_key.label")} rules={[formRule]}> <Form.Item name="privateKey" label={t("workflow_node.upload.form.private_key.label")} rules={[formRule]}>
<Input.TextArea readOnly autoSize={{ minRows: 5, maxRows: 10 }} placeholder={t("workflow_node.upload.form.private_key.placeholder")} /> <Input.TextArea readOnly autoSize={{ minRows: 5, maxRows: 5 }} placeholder={t("workflow_node.upload.form.private_key.placeholder")} />
</Form.Item> </Form.Item>
<Form.Item> <Form.Item>

View File

@ -3,8 +3,11 @@ import { type WorkflowModel } from "./workflow";
export interface CertificateModel extends BaseModel { export interface CertificateModel extends BaseModel {
source: string; source: string;
subjectAltNames: string; subjectAltNames: string;
serialNumber: string;
certificate: string; certificate: string;
privateKey: string; privateKey: string;
issuer: string;
keyAlgorithm: string;
effectAt: ISO8601String; effectAt: ISO8601String;
expireAt: ISO8601String; expireAt: ISO8601String;
workflowId: string; workflowId: string;

View File

@ -122,6 +122,7 @@ export type WorkflowNodeConfigForStart = {
export type WorkflowNodeConfigForApply = { export type WorkflowNodeConfigForApply = {
domains: string; domains: string;
contactEmail: string; contactEmail: string;
challengeType: string;
provider: string; provider: string;
providerAccessId: string; providerAccessId: string;
providerConfig?: Record<string, unknown>; providerConfig?: Record<string, unknown>;
@ -276,21 +277,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) => { return produce(node, (draft) => {
let current = draft; let current = draft;
while (current) { 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; targetNode.next = current.next;
current.next = targetNode; current.next = targetNode;
break; 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; targetNode.branches![0].next = current.next;
current.next = targetNode; current.next = targetNode;
break; break;
} }
if (current.type === WorkflowNodeType.Branch || current.type === WorkflowNodeType.ExecuteResultBranch) { 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; current = current.next as WorkflowNode;
} }
@ -382,15 +383,15 @@ export const removeBranch = (node: WorkflowNode, branchNodeId: string, branchInd
}); });
}; };
// 1 个分支的节点,不应该能获取到相邻分支上节点的输出 export const getWorkflowOutputBeforeId = (root: WorkflowNode, nodeId: string, type: string): WorkflowNode[] => {
export const getWorkflowOutputBeforeId = (node: WorkflowNode, id: string, type: string): WorkflowNode[] => { // 1 个分支的节点,不应该能获取到相邻分支上节点的输出
const output: WorkflowNode[] = []; const output: WorkflowNode[] = [];
const traverse = (current: WorkflowNode, output: WorkflowNode[]) => { const traverse = (current: WorkflowNode, output: WorkflowNode[]) => {
if (!current) { if (!current) {
return false; return false;
} }
if (current.id === id) { if (current.id === nodeId) {
return true; return true;
} }
@ -422,7 +423,7 @@ export const getWorkflowOutputBeforeId = (node: WorkflowNode, id: string, type:
return traverse(current.next as WorkflowNode, output); return traverse(current.next as WorkflowNode, output);
}; };
traverse(node, output); traverse(root, output);
return output; return output;
}; };
@ -446,21 +447,3 @@ export const isAllNodesValidated = (node: WorkflowNode): boolean => {
return true; 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: "",
};
}
};

View File

@ -1,4 +1,4 @@
import type { WorkflowModel } from "./workflow"; import { type WorkflowModel } from "./workflow";
export interface WorkflowRunModel extends BaseModel { export interface WorkflowRunModel extends BaseModel {
workflowId: string; workflowId: string;
@ -16,13 +16,13 @@ export interface WorkflowRunModel extends BaseModel {
export type WorkflowRunLog = { export type WorkflowRunLog = {
nodeId: string; nodeId: string;
nodeName: string; nodeName: string;
outputs?: WorkflowRunLogOutput[]; records?: WorkflowRunLogRecord[];
error?: string; error?: string;
}; };
export type WorkflowRunLogOutput = { export type WorkflowRunLogRecord = {
time: ISO8601String; time: ISO8601String;
title: string; level: string;
content: string; content: string;
error?: string; error?: string;
}; };

View File

@ -15,11 +15,15 @@
"certificate.props.validity.expiration": "Expire on {{date}}", "certificate.props.validity.expiration": "Expire on {{date}}",
"certificate.props.validity.filter.expire_soon": "Expire soon", "certificate.props.validity.filter.expire_soon": "Expire soon",
"certificate.props.validity.filter.expired": "Expired", "certificate.props.validity.filter.expired": "Expired",
"certificate.props.brand": "Brand",
"certificate.props.source": "Source", "certificate.props.source": "Source",
"certificate.props.source.workflow": "Workflow", "certificate.props.source.workflow": "Workflow",
"certificate.props.source.upload": "Upload", "certificate.props.source.upload": "Upload",
"certificate.props.certificate": "Certificate chain", "certificate.props.certificate": "Certificate chain",
"certificate.props.private_key": "Private key", "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.created_at": "Created at",
"certificate.props.updated_at": "Updated at" "certificate.props.updated_at": "Updated at"
} }

View File

@ -18,7 +18,7 @@
"workflow_node.start.form.trigger_cron.label": "Cron expression", "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.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.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.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.<br><br>Reference links:<br>1. <a href=\"https://letsencrypt.org/docs/rate-limits/\" target=\"_blank\">Lets Encrypt rate limits</a><br>2. <a href=\"https://letsencrypt.org/docs/faq/#why-should-my-let-s-encrypt-acme-client-run-at-a-random-time\" target=\"_blank\">Why should my Lets Encrypt (ACME) client run at a random time?</a>", "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.<br><br>Reference links:<br>1. <a href=\"https://letsencrypt.org/docs/rate-limits/\" target=\"_blank\">Lets Encrypt rate limits</a><br>2. <a href=\"https://letsencrypt.org/docs/faq/#why-should-my-let-s-encrypt-acme-client-run-at-a-random-time\" target=\"_blank\">Why should my Lets Encrypt (ACME) client run at a random time?</a>",

View File

@ -16,5 +16,12 @@
"workflow_run.props.trigger.auto": "Timing", "workflow_run.props.trigger.auto": "Timing",
"workflow_run.props.trigger.manual": "Manual", "workflow_run.props.trigger.manual": "Manual",
"workflow_run.props.started_at": "Started at", "workflow_run.props.started_at": "Started at",
"workflow_run.props.ended_at": "Ended at" "workflow_run.props.ended_at": "Ended at",
"workflow_run.logs": "Logs",
"workflow_run.artifacts": "Artifacts",
"workflow_run_artifact.props.type": "Type",
"workflow_run_artifact.props.type.certificate": "Certificate",
"workflow_run_artifact.props.name": "Name"
} }

View File

@ -15,11 +15,15 @@
"certificate.props.validity.expiration": "{{date}} 到期", "certificate.props.validity.expiration": "{{date}} 到期",
"certificate.props.validity.filter.expire_soon": "即将到期", "certificate.props.validity.filter.expire_soon": "即将到期",
"certificate.props.validity.filter.expired": "已到期", "certificate.props.validity.filter.expired": "已到期",
"certificate.props.brand": "证书品牌",
"certificate.props.source": "来源", "certificate.props.source": "来源",
"certificate.props.source.workflow": "工作流", "certificate.props.source.workflow": "工作流",
"certificate.props.source.upload": "用户上传", "certificate.props.source.upload": "用户上传",
"certificate.props.certificate": "证书内容", "certificate.props.certificate": "证书内容",
"certificate.props.private_key": "私钥内容", "certificate.props.private_key": "私钥内容",
"certificate.props.serial_number": "证书序列号",
"certificate.props.key_algorithm": "证书算法",
"certificate.props.issuer": "颁发者",
"certificate.props.created_at": "创建时间", "certificate.props.created_at": "创建时间",
"certificate.props.updated_at": "更新时间" "certificate.props.updated_at": "更新时间"
} }

View File

@ -18,7 +18,7 @@
"workflow_node.start.form.trigger_cron.label": "Cron 表达式", "workflow_node.start.form.trigger_cron.label": "Cron 表达式",
"workflow_node.start.form.trigger_cron.placeholder": "请输入 Cron 表达式", "workflow_node.start.form.trigger_cron.placeholder": "请输入 Cron 表达式",
"workflow_node.start.form.trigger_cron.errmsg.invalid": "请输入正确的 Cron 表达式", "workflow_node.start.form.trigger_cron.errmsg.invalid": "请输入正确的 Cron 表达式",
"workflow_node.start.form.trigger_cron.tooltip": "支持使用任意值(即 <strong>*</strong>)、值列表分隔符(即 <strong>,</strong>)、值的范围(即 <strong>-</strong>)、步骤值(即 <strong>/</strong>)等四种表达式时区以服务器设置为准。", "workflow_node.start.form.trigger_cron.tooltip": "五段式表达式,支持使用任意值(即 <strong>*</strong>)、值列表分隔符(即 <strong>,</strong>)、值的范围(即 <strong>-</strong>)、步骤值(即 <strong>/</strong>)等四种表达式时区以服务器设置为准。",
"workflow_node.start.form.trigger_cron.extra": "预计最近 5 次执行时间:", "workflow_node.start.form.trigger_cron.extra": "预计最近 5 次执行时间:",
"workflow_node.start.form.trigger_cron.guide": "小贴士:如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。<br><br>参考链接:<br>1. <a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">Lets Encrypt 速率限制</a><br>2. <a href=\"https://letsencrypt.org/zh-cn/docs/faq/#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E7%9A%84-let-s-encrypt-acme-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%AF%E5%8A%A8%E6%97%B6%E9%97%B4%E5%BA%94%E5%BD%93%E9%9A%8F%E6%9C%BA\" target=\"_blank\">为什么我的 Lets Encrypt (ACME) 客户端启动时间应当随机?</a>", "workflow_node.start.form.trigger_cron.guide": "小贴士:如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。<br><br>参考链接:<br>1. <a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">Lets Encrypt 速率限制</a><br>2. <a href=\"https://letsencrypt.org/zh-cn/docs/faq/#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E7%9A%84-let-s-encrypt-acme-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%AF%E5%8A%A8%E6%97%B6%E9%97%B4%E5%BA%94%E5%BD%93%E9%9A%8F%E6%9C%BA\" target=\"_blank\">为什么我的 Lets Encrypt (ACME) 客户端启动时间应当随机?</a>",

View File

@ -16,5 +16,12 @@
"workflow_run.props.trigger.auto": "定时执行", "workflow_run.props.trigger.auto": "定时执行",
"workflow_run.props.trigger.manual": "手动执行", "workflow_run.props.trigger.manual": "手动执行",
"workflow_run.props.started_at": "开始时间", "workflow_run.props.started_at": "开始时间",
"workflow_run.props.ended_at": "完成时间" "workflow_run.props.ended_at": "完成时间",
"workflow_run.logs": "日志",
"workflow_run.artifacts": "输出产物",
"workflow_run_artifact.props.type": "类型",
"workflow_run_artifact.props.type.certificate": "证书",
"workflow_run_artifact.props.name": "名称"
} }

View File

@ -1,13 +1,8 @@
:root { :root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400; font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none; font-synthesis: none;
line-height: 1.5;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
@ -15,8 +10,7 @@
body { body {
margin: 0; margin: 0;
display: flex; padding: 0;
place-items: center;
min-width: 320px; min-width: 320px;
min-height: 100vh; min-height: 100vh;
} }

View File

@ -5,6 +5,7 @@ import dayjsUtc from "dayjs/plugin/utc";
import App from "./App"; import App from "./App";
import "./i18n"; import "./i18n";
import "./index.css";
import "./global.css"; import "./global.css";
dayjs.extend(dayjsUtc); dayjs.extend(dayjsUtc);

View File

@ -207,7 +207,7 @@ const AccessList = () => {
setPageSize(pageSize); setPageSize(pageSize);
}, },
}} }}
rowKey={(record: AccessModel) => record.id} rowKey={(record) => record.id}
scroll={{ x: "max(100%, 960px)" }} scroll={{ x: "max(100%, 960px)" }}
/> />
</div> </div>

View File

@ -107,6 +107,16 @@ const CertificateList = () => {
); );
}, },
}, },
{
key: "issuer",
title: t("certificate.props.brand"),
render: (_, record) => (
<Space className="max-w-full" direction="vertical" size={4}>
<Typography.Text>{record.issuer}</Typography.Text>
<Typography.Text>{record.keyAlgorithm}</Typography.Text>
</Space>
),
},
{ {
key: "source", key: "source",
title: t("certificate.props.source"), title: t("certificate.props.source"),
@ -250,7 +260,7 @@ const CertificateList = () => {
dataSource={tableData} dataSource={tableData}
loading={loading} loading={loading}
locale={{ locale={{
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={loadedError ? getErrMsg(loadedError) : t("certificate.nodata")} />, emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={getErrMsg(loadedError ?? t("certificate.nodata"))} />,
}} }}
pagination={{ pagination={{
current: page, current: page,
@ -266,7 +276,7 @@ const CertificateList = () => {
setPageSize(pageSize); setPageSize(pageSize);
}, },
}} }}
rowKey={(record: CertificateModel) => record.id} rowKey={(record) => record.id}
scroll={{ x: "max(100%, 960px)" }} scroll={{ x: "max(100%, 960px)" }}
/> />
</div> </div>

View File

@ -7,16 +7,15 @@ import {
ClockCircleOutlined as ClockCircleOutlinedIcon, ClockCircleOutlined as ClockCircleOutlinedIcon,
CloseCircleOutlined as CloseCircleOutlinedIcon, CloseCircleOutlined as CloseCircleOutlinedIcon,
LockOutlined as LockOutlinedIcon, LockOutlined as LockOutlinedIcon,
PauseCircleOutlined as PauseCircleOutlinedIcon,
PlusOutlined as PlusOutlinedIcon, PlusOutlined as PlusOutlinedIcon,
SelectOutlined as SelectOutlinedIcon, SelectOutlined as SelectOutlinedIcon,
SendOutlined as SendOutlinedIcon, SendOutlined as SendOutlinedIcon,
StopOutlined as StopOutlinedIcon,
SyncOutlined as SyncOutlinedIcon, SyncOutlined as SyncOutlinedIcon,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-components"; import { PageHeader } from "@ant-design/pro-components";
import { useRequest } from "ahooks"; import { useRequest } from "ahooks";
import type { TableProps } from "antd"; import { Button, Card, Col, Divider, Empty, Flex, Grid, Row, Space, Statistic, Table, type TableProps, Tag, Typography, notification, theme } from "antd";
import { Button, Card, Col, Divider, Empty, Flex, Grid, Row, Space, Statistic, Table, Tag, Typography, notification, theme } from "antd";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { import {
CalendarClock as CalendarClockIcon, CalendarClock as CalendarClockIcon,
@ -89,7 +88,6 @@ const Dashboard = () => {
const workflow = record.expand?.workflowId; const workflow = record.expand?.workflowId;
return ( return (
<Typography.Link <Typography.Link
type="secondary"
ellipsis ellipsis
onClick={() => { onClick={() => {
if (workflow) { if (workflow) {
@ -129,7 +127,7 @@ const Dashboard = () => {
); );
} else if (record.status === WORKFLOW_RUN_STATUSES.CANCELED) { } else if (record.status === WORKFLOW_RUN_STATUSES.CANCELED) {
return ( return (
<Tag icon={<PauseCircleOutlinedIcon />} color="warning"> <Tag icon={<StopOutlinedIcon />} color="warning">
{t("workflow_run.props.status.canceled")} {t("workflow_run.props.status.canceled")}
</Tag> </Tag>
); );
@ -178,7 +176,7 @@ const Dashboard = () => {
() => { () => {
return listWorkflowRuns({ return listWorkflowRuns({
page: 1, page: 1,
perPage: 5, perPage: 9,
expand: true, expand: true,
}); });
}, },
@ -286,8 +284,9 @@ const Dashboard = () => {
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />, emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />,
}} }}
pagination={false} pagination={false}
rowKey={(record: WorkflowRunModel) => record.id} rowKey={(record) => record.id}
scroll={{ x: "max(100%, 960px)" }} scroll={{ x: "max(100%, 960px)" }}
size="small"
/> />
</Card> </Card>
</Flex> </Flex>

View File

@ -34,7 +34,7 @@ const SettingsPassword = () => {
onSubmit: async (values) => { onSubmit: async (values) => {
try { try {
await authWithPassword(getAuthStore().record!.email, values.oldPassword); 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")); messageApi.success(t("common.text.operation_succeeded"));

View File

@ -42,7 +42,6 @@ const WorkflowDetail = () => {
useZustandShallowSelector(["workflow", "initialized", "init", "destroy", "setEnabled", "release", "discard"]) useZustandShallowSelector(["workflow", "initialized", "init", "destroy", "setEnabled", "release", "discard"])
); );
useEffect(() => { useEffect(() => {
// TODO: loading & error
workflowState.init(workflowId!); workflowState.init(workflowId!);
return () => { return () => {
@ -52,7 +51,7 @@ const WorkflowDetail = () => {
const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration"); const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration");
const [isRunning, setIsRunning] = useState(false); const [isPendingOrRunning, setIsPendingOrRunning] = useState(false);
const lastRunStatus = useMemo(() => workflow.lastRunStatus, [workflow]); const lastRunStatus = useMemo(() => workflow.lastRunStatus, [workflow]);
const [allowDiscard, setAllowDiscard] = useState(false); const [allowDiscard, setAllowDiscard] = useState(false);
@ -60,14 +59,14 @@ const WorkflowDetail = () => {
const [allowRun, setAllowRun] = useState(false); const [allowRun, setAllowRun] = useState(false);
useEffect(() => { useEffect(() => {
setIsRunning(lastRunStatus == WORKFLOW_RUN_STATUSES.RUNNING); setIsPendingOrRunning(lastRunStatus == WORKFLOW_RUN_STATUSES.PENDING || lastRunStatus == WORKFLOW_RUN_STATUSES.RUNNING);
}, [lastRunStatus]); }, [lastRunStatus]);
useEffect(() => { useEffect(() => {
if (!!workflowId && isRunning) { if (!!workflowId && isPendingOrRunning) {
subscribeWorkflow(workflowId, (e) => { subscribeWorkflow(workflowId, (cb) => {
if (e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.RUNNING) { if (cb.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.PENDING && cb.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.RUNNING) {
setIsRunning(false); setIsPendingOrRunning(false);
unsubscribeWorkflow(workflowId); unsubscribeWorkflow(workflowId);
} }
}); });
@ -76,15 +75,15 @@ const WorkflowDetail = () => {
unsubscribeWorkflow(workflowId); unsubscribeWorkflow(workflowId);
}; };
} }
}, [workflowId, isRunning]); }, [workflowId, isPendingOrRunning]);
useEffect(() => { useEffect(() => {
const hasReleased = !!workflow.content; const hasReleased = !!workflow.content;
const hasChanges = workflow.hasDraft! || !isEqual(workflow.draft, workflow.content); const hasChanges = workflow.hasDraft! || !isEqual(workflow.draft, workflow.content);
setAllowDiscard(!isRunning && hasReleased && hasChanges); setAllowDiscard(!isPendingOrRunning && hasReleased && hasChanges);
setAllowRelease(!isRunning && hasChanges); setAllowRelease(!isPendingOrRunning && hasChanges);
setAllowRun(hasReleased); setAllowRun(hasReleased);
}, [workflow.content, workflow.draft, workflow.hasDraft, isRunning]); }, [workflow.content, workflow.draft, workflow.hasDraft, isPendingOrRunning]);
const handleEnableChange = async () => { const handleEnableChange = async () => {
if (!workflow.enabled && (!workflow.content || !isAllNodesValidated(workflow.content))) { if (!workflow.enabled && (!workflow.content || !isAllNodesValidated(workflow.content))) {
@ -174,12 +173,12 @@ const WorkflowDetail = () => {
let unsubscribeFn: Awaited<ReturnType<typeof subscribeWorkflow>> | undefined = undefined; let unsubscribeFn: Awaited<ReturnType<typeof subscribeWorkflow>> | undefined = undefined;
try { try {
setIsRunning(true); setIsPendingOrRunning(true);
// subscribe before running workflow // subscribe before running workflow
unsubscribeFn = await subscribeWorkflow(workflowId!, (e) => { 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); setIsPendingOrRunning(false);
unsubscribeFn?.(); unsubscribeFn?.();
} }
}); });
@ -188,7 +187,7 @@ const WorkflowDetail = () => {
messageApi.info(t("workflow.detail.orchestration.action.run.prompt")); messageApi.info(t("workflow.detail.orchestration.action.run.prompt"));
} catch (err) { } catch (err) {
setIsRunning(false); setIsPendingOrRunning(false);
unsubscribeFn?.(); unsubscribeFn?.();
console.error(err); console.error(err);
@ -279,7 +278,7 @@ const WorkflowDetail = () => {
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
<Space> <Space>
<Button disabled={!allowRun} icon={<CaretRightOutlinedIcon />} loading={isRunning} type="primary" onClick={handleRunClick}> <Button disabled={!allowRun} icon={<CaretRightOutlinedIcon />} loading={isPendingOrRunning} type="primary" onClick={handleRunClick}>
{t("workflow.detail.orchestration.action.run")} {t("workflow.detail.orchestration.action.run")}
</Button> </Button>

View File

@ -7,8 +7,8 @@ import {
CloseCircleOutlined as CloseCircleOutlinedIcon, CloseCircleOutlined as CloseCircleOutlinedIcon,
DeleteOutlined as DeleteOutlinedIcon, DeleteOutlined as DeleteOutlinedIcon,
EditOutlined as EditOutlinedIcon, EditOutlined as EditOutlinedIcon,
PauseCircleOutlined as PauseCircleOutlinedIcon,
PlusOutlined as PlusOutlinedIcon, PlusOutlined as PlusOutlinedIcon,
StopOutlined as StopOutlinedIcon,
SyncOutlined as SyncOutlinedIcon, SyncOutlined as SyncOutlinedIcon,
} from "@ant-design/icons"; } from "@ant-design/icons";
@ -170,7 +170,7 @@ const WorkflowList = () => {
} else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.FAILED) { } else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.FAILED) {
icon = <CloseCircleOutlinedIcon style={{ color: themeToken.colorError }} />; icon = <CloseCircleOutlinedIcon style={{ color: themeToken.colorError }} />;
} else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.CANCELED) { } else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.CANCELED) {
icon = <PauseCircleOutlinedIcon style={{ color: themeToken.colorWarning }} />; icon = <StopOutlinedIcon style={{ color: themeToken.colorWarning }} />;
} }
return ( return (
@ -350,7 +350,7 @@ const WorkflowList = () => {
dataSource={tableData} dataSource={tableData}
loading={loading} loading={loading}
locale={{ locale={{
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={loadedError ? getErrMsg(loadedError) : t("workflow.nodata")} />, emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={getErrMsg(loadedError ?? t("workflow.nodata"))} />,
}} }}
pagination={{ pagination={{
current: page, current: page,
@ -366,7 +366,7 @@ const WorkflowList = () => {
setPageSize(pageSize); setPageSize(pageSize);
}, },
}} }}
rowKey={(record: WorkflowModel) => record.id} rowKey={(record) => record.id}
scroll={{ x: "max(100%, 960px)" }} scroll={{ x: "max(100%, 960px)" }}
/> />
</div> </div>

View File

@ -6,3 +6,11 @@ export const getPocketBase = () => {
pb = new PocketBase("/"); pb = new PocketBase("/");
return pb; return pb;
}; };
export const COLLECTION_NAME_ADMIN = "_superusers";
export const COLLECTION_NAME_ACCESS = "access";
export const COLLECTION_NAME_CERTIFICATE = "certificate";
export const COLLECTION_NAME_SETTINGS = "settings";
export const COLLECTION_NAME_WORKFLOW = "workflow";
export const COLLECTION_NAME_WORKFLOW_RUN = "workflow_run";
export const COLLECTION_NAME_WORKFLOW_OUTPUT = "workflow_output";

View File

@ -1,12 +1,10 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { type AccessModel } from "@/domain/access"; import { type AccessModel } from "@/domain/access";
import { getPocketBase } from "./_pocketbase"; import { COLLECTION_NAME_ACCESS, getPocketBase } from "./_pocketbase";
const COLLECTION_NAME = "access";
export const list = async () => { export const list = async () => {
return await getPocketBase().collection(COLLECTION_NAME).getFullList<AccessModel>({ return await getPocketBase().collection(COLLECTION_NAME_ACCESS).getFullList<AccessModel>({
filter: "deleted=null", filter: "deleted=null",
sort: "-created", sort: "-created",
requestKey: null, requestKey: null,
@ -15,15 +13,15 @@ export const list = async () => {
export const save = async (record: MaybeModelRecord<AccessModel>) => { export const save = async (record: MaybeModelRecord<AccessModel>) => {
if (record.id) { if (record.id) {
return await getPocketBase().collection(COLLECTION_NAME).update<AccessModel>(record.id, record); return await getPocketBase().collection(COLLECTION_NAME_ACCESS).update<AccessModel>(record.id, record);
} }
return await getPocketBase().collection(COLLECTION_NAME).create<AccessModel>(record); return await getPocketBase().collection(COLLECTION_NAME_ACCESS).create<AccessModel>(record);
}; };
export const remove = async (record: MaybeModelRecordWithId<AccessModel>) => { export const remove = async (record: MaybeModelRecordWithId<AccessModel>) => {
await getPocketBase() await getPocketBase()
.collection(COLLECTION_NAME) .collection(COLLECTION_NAME_ACCESS)
.update<AccessModel>(record.id!, { deleted: dayjs.utc().format("YYYY-MM-DD HH:mm:ss") }); .update<AccessModel>(record.id!, { deleted: dayjs.utc().format("YYYY-MM-DD HH:mm:ss") });
return true; return true;
}; };

View File

@ -1,17 +1,15 @@
import { getPocketBase } from "./_pocketbase"; import { COLLECTION_NAME_ADMIN, getPocketBase } from "./_pocketbase";
const COLLECTION_NAME = "_superusers";
export const authWithPassword = (username: string, password: string) => { export const authWithPassword = (username: string, password: string) => {
return getPocketBase().collection(COLLECTION_NAME).authWithPassword(username, password); return getPocketBase().collection(COLLECTION_NAME_ADMIN).authWithPassword(username, password);
}; };
export const getAuthStore = () => { export const getAuthStore = () => {
return getPocketBase().authStore; return getPocketBase().authStore;
}; };
export const save = (data: { email: string } | { password: string }) => { export const save = (data: { email: string } | { password: string; passwordConfirm: string }) => {
return getPocketBase() return getPocketBase()
.collection(COLLECTION_NAME) .collection(COLLECTION_NAME_ADMIN)
.update(getAuthStore().record?.id || "", data); .update(getAuthStore().record?.id || "", data);
}; };

View File

@ -2,9 +2,7 @@ import dayjs from "dayjs";
import { type RecordListOptions } from "pocketbase"; import { type RecordListOptions } from "pocketbase";
import { type CertificateModel } from "@/domain/certificate"; import { type CertificateModel } from "@/domain/certificate";
import { getPocketBase } from "./_pocketbase"; import { COLLECTION_NAME_CERTIFICATE, getPocketBase } from "./_pocketbase";
const COLLECTION_NAME = "certificate";
export type ListCertificateRequest = { export type ListCertificateRequest = {
page?: number; page?: number;
@ -35,12 +33,29 @@ export const list = async (request: ListCertificateRequest) => {
}); });
} }
return pb.collection(COLLECTION_NAME).getList<CertificateModel>(page, perPage, options); return pb.collection(COLLECTION_NAME_CERTIFICATE).getList<CertificateModel>(page, perPage, options);
};
export const listByWorkflowRunId = async (workflowRunId: string) => {
const pb = getPocketBase();
const options: RecordListOptions = {
filter: pb.filter("workflowRunId={:workflowRunId}", {
workflowRunId: workflowRunId,
}),
sort: "-created",
requestKey: null,
};
const items = await pb.collection(COLLECTION_NAME_CERTIFICATE).getFullList<CertificateModel>(options);
return {
totalItems: items.length,
items: items,
};
}; };
export const remove = async (record: MaybeModelRecordWithId<CertificateModel>) => { export const remove = async (record: MaybeModelRecordWithId<CertificateModel>) => {
await getPocketBase() await getPocketBase()
.collection(COLLECTION_NAME) .collection(COLLECTION_NAME_CERTIFICATE)
.update<CertificateModel>(record.id!, { deleted: dayjs.utc().format("YYYY-MM-DD HH:mm:ss") }); .update<CertificateModel>(record.id!, { deleted: dayjs.utc().format("YYYY-MM-DD HH:mm:ss") });
return true; return true;
}; };

View File

@ -1,13 +1,11 @@
import { ClientResponseError } from "pocketbase"; import { ClientResponseError } from "pocketbase";
import { type SettingsModel, type SettingsNames } from "@/domain/settings"; import { type SettingsModel, type SettingsNames } from "@/domain/settings";
import { getPocketBase } from "./_pocketbase"; import { COLLECTION_NAME_SETTINGS, getPocketBase } from "./_pocketbase";
const COLLECTION_NAME = "settings";
export const get = async <T extends NonNullable<unknown>>(name: SettingsNames) => { export const get = async <T extends NonNullable<unknown>>(name: SettingsNames) => {
try { try {
const resp = await getPocketBase().collection(COLLECTION_NAME).getFirstListItem<SettingsModel<T>>(`name='${name}'`, { const resp = await getPocketBase().collection(COLLECTION_NAME_SETTINGS).getFirstListItem<SettingsModel<T>>(`name='${name}'`, {
requestKey: null, requestKey: null,
}); });
return resp; return resp;
@ -25,8 +23,8 @@ export const get = async <T extends NonNullable<unknown>>(name: SettingsNames) =
export const save = async <T extends NonNullable<unknown>>(record: MaybeModelRecordWithId<SettingsModel<T>>) => { export const save = async <T extends NonNullable<unknown>>(record: MaybeModelRecordWithId<SettingsModel<T>>) => {
if (record.id) { if (record.id) {
return await getPocketBase().collection(COLLECTION_NAME).update<SettingsModel<T>>(record.id, record); return await getPocketBase().collection(COLLECTION_NAME_SETTINGS).update<SettingsModel<T>>(record.id, record);
} }
return await getPocketBase().collection(COLLECTION_NAME).create<SettingsModel<T>>(record); return await getPocketBase().collection(COLLECTION_NAME_SETTINGS).create<SettingsModel<T>>(record);
}; };

View File

@ -1,9 +1,7 @@
import { type RecordListOptions, type RecordSubscription } from "pocketbase"; import { type RecordListOptions, type RecordSubscription } from "pocketbase";
import { type WorkflowModel } from "@/domain/workflow"; import { type WorkflowModel } from "@/domain/workflow";
import { getPocketBase } from "./_pocketbase"; import { COLLECTION_NAME_WORKFLOW, getPocketBase } from "./_pocketbase";
const COLLECTION_NAME = "workflow";
export type ListWorkflowRequest = { export type ListWorkflowRequest = {
page?: number; page?: number;
@ -26,11 +24,11 @@ export const list = async (request: ListWorkflowRequest) => {
options.filter = pb.filter("enabled={:enabled}", { enabled: request.enabled }); options.filter = pb.filter("enabled={:enabled}", { enabled: request.enabled });
} }
return await pb.collection(COLLECTION_NAME).getList<WorkflowModel>(page, perPage, options); return await pb.collection(COLLECTION_NAME_WORKFLOW).getList<WorkflowModel>(page, perPage, options);
}; };
export const get = async (id: string) => { export const get = async (id: string) => {
return await getPocketBase().collection(COLLECTION_NAME).getOne<WorkflowModel>(id, { return await getPocketBase().collection(COLLECTION_NAME_WORKFLOW).getOne<WorkflowModel>(id, {
requestKey: null, requestKey: null,
}); });
}; };
@ -38,25 +36,21 @@ export const get = async (id: string) => {
export const save = async (record: MaybeModelRecord<WorkflowModel>) => { export const save = async (record: MaybeModelRecord<WorkflowModel>) => {
if (record.id) { if (record.id) {
return await getPocketBase() return await getPocketBase()
.collection(COLLECTION_NAME) .collection(COLLECTION_NAME_WORKFLOW)
.update<WorkflowModel>(record.id as string, record); .update<WorkflowModel>(record.id as string, record);
} }
return await getPocketBase().collection(COLLECTION_NAME).create<WorkflowModel>(record); return await getPocketBase().collection(COLLECTION_NAME_WORKFLOW).create<WorkflowModel>(record);
}; };
export const remove = async (record: MaybeModelRecordWithId<WorkflowModel>) => { export const remove = async (record: MaybeModelRecordWithId<WorkflowModel>) => {
return await getPocketBase().collection(COLLECTION_NAME).delete(record.id); return await getPocketBase().collection(COLLECTION_NAME_WORKFLOW).delete(record.id);
}; };
export const subscribe = async (id: string, cb: (e: RecordSubscription<WorkflowModel>) => void) => { export const subscribe = async (id: string, cb: (e: RecordSubscription<WorkflowModel>) => void) => {
const pb = getPocketBase(); return getPocketBase().collection(COLLECTION_NAME_WORKFLOW).subscribe(id, cb);
return pb.collection("workflow").subscribe(id, cb);
}; };
export const unsubscribe = async (id: string) => { export const unsubscribe = async (id: string) => {
const pb = getPocketBase(); return getPocketBase().collection(COLLECTION_NAME_WORKFLOW).unsubscribe(id);
return pb.collection("workflow").unsubscribe(id);
}; };

View File

@ -1,8 +1,8 @@
import { type WorkflowRunModel } from "@/domain/workflowRun"; import { type RecordSubscription } from "pocketbase";
import { getPocketBase } from "./_pocketbase"; import { type WorkflowRunModel } from "@/domain/workflowRun";
const COLLECTION_NAME = "workflow_run"; import { COLLECTION_NAME_WORKFLOW_RUN, getPocketBase } from "./_pocketbase";
export type ListWorkflowRunsRequest = { export type ListWorkflowRunsRequest = {
workflowId?: string; workflowId?: string;
@ -23,7 +23,7 @@ export const list = async (request: ListWorkflowRunsRequest) => {
} }
return await getPocketBase() return await getPocketBase()
.collection(COLLECTION_NAME) .collection(COLLECTION_NAME_WORKFLOW_RUN)
.getList<WorkflowRunModel>(page, perPage, { .getList<WorkflowRunModel>(page, perPage, {
filter: getPocketBase().filter(filter, params), filter: getPocketBase().filter(filter, params),
sort: "-created", sort: "-created",
@ -33,5 +33,13 @@ export const list = async (request: ListWorkflowRunsRequest) => {
}; };
export const remove = async (record: MaybeModelRecordWithId<WorkflowRunModel>) => { export const remove = async (record: MaybeModelRecordWithId<WorkflowRunModel>) => {
return await getPocketBase().collection(COLLECTION_NAME).delete(record.id); return await getPocketBase().collection(COLLECTION_NAME_WORKFLOW_RUN).delete(record.id);
};
export const subscribe = async (id: string, cb: (e: RecordSubscription<WorkflowRunModel>) => void) => {
return getPocketBase().collection(COLLECTION_NAME_WORKFLOW_RUN).subscribe(id, cb);
};
export const unsubscribe = async (id: string) => {
return getPocketBase().collection(COLLECTION_NAME_WORKFLOW_RUN).unsubscribe(id);
}; };

View File

@ -25,14 +25,14 @@ export type WorkflowState = {
discard(): void; discard(): void;
destroy(): void; destroy(): void;
addNode: (node: WorkflowNode, preId: string) => void; addNode: (node: WorkflowNode, previousNodeId: string) => void;
updateNode: (node: WorkflowNode) => void; updateNode: (node: WorkflowNode) => void;
removeNode: (nodeId: string) => void; removeNode: (nodeId: string) => void;
addBranch: (branchId: string) => void; addBranch: (branchId: string) => void;
removeBranch: (branchId: string, index: number) => void; removeBranch: (branchId: string, index: number) => void;
getWorkflowOuptutBeforeId: (id: string, type: string) => WorkflowNode[]; getWorkflowOuptutBeforeId: (nodeId: string, type: string) => WorkflowNode[];
}; };
export const useWorkflowStore = create<WorkflowState>((set, get) => ({ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
@ -143,10 +143,10 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
}); });
}, },
addNode: async (node: WorkflowNode, preId: string) => { addNode: async (node: WorkflowNode, previousNodeId: string) => {
if (!get().initialized) throw "Workflow not initialized yet"; 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({ const resp = await saveWorkflow({
id: get().workflow.id!, id: get().workflow.id!,
draft: root, draft: root,
@ -243,7 +243,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
}); });
}, },
getWorkflowOuptutBeforeId: (id: string, type: string) => { getWorkflowOuptutBeforeId: (nodeId: string, type: string) => {
return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, id, type); return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, nodeId, type);
}, },
})); }));

View File

@ -3,6 +3,8 @@
export const validCronExpression = (expr: string): boolean => { export const validCronExpression = (expr: string): boolean => {
try { try {
parseExpression(expr); parseExpression(expr);
if (expr.trim().split(" ").length !== 5) return false; // pocketbase 后端仅支持五段式的表达式
return true; return true;
} catch { } catch {
return false; return false;
@ -10,9 +12,8 @@ export const validCronExpression = (expr: string): boolean => {
}; };
export const getNextCronExecutions = (expr: string, times = 1): Date[] => { export const getNextCronExecutions = (expr: string, times = 1): Date[] => {
if (!expr) return []; if (!validCronExpression(expr)) return [];
try {
const now = new Date(); const now = new Date();
const cron = parseExpression(expr, { currentDate: now, iterator: true }); const cron = parseExpression(expr, { currentDate: now, iterator: true });
@ -22,7 +23,4 @@ export const getNextCronExecutions = (expr: string, times = 1): Date[] => {
result.push(next.value.toDate()); result.push(next.value.toDate());
} }
return result; return result;
} catch {
return [];
}
}; };

View File

@ -7,13 +7,13 @@ export const getErrMsg = (error: unknown): string => {
return error.message; return error.message;
} else if (typeof error === "object" && error != null) { } else if (typeof error === "object" && error != null) {
if ("message" in error) { if ("message" in error) {
return String(error.message); return getErrMsg(error.message);
} else if ("msg" in error) { } else if ("msg" in error) {
return String(error.msg); return getErrMsg(error.msg);
} }
} else if (typeof error === "string") { } else if (typeof error === "string") {
return error; return error || "Unknown error";
} }
return String(error ?? "Unknown error"); return "Unknown error";
}; };