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/pkg/sftp v1.13.7
github.com/pocketbase/dbx v1.11.0
github.com/pocketbase/pocketbase v0.24.4
github.com/pocketbase/pocketbase v0.25.0
github.com/povsister/scp v0.0.0-20240802064259-28781e87b246
github.com/qiniu/go-sdk/v7 v7.25.2
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1084
@ -42,7 +42,7 @@ require (
github.com/volcengine/volc-sdk-golang v1.0.193
github.com/volcengine/volcengine-go-sdk v1.0.178
golang.org/x/crypto v0.32.0
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c
k8s.io/api v0.32.1
k8s.io/apimachinery v0.32.1
k8s.io/client-go v0.32.1
@ -108,7 +108,6 @@ require (
)
require (
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/dcdn-20180115/v3 v3.5.0
@ -120,25 +119,25 @@ require (
github.com/aliyun/alibaba-cloud-sdk-go v1.63.83 // indirect
github.com/aliyun/credentials-go v1.4.3 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2 v1.33.0
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect
github.com/aws/aws-sdk-go-v2/config v1.29.1
github.com/aws/aws-sdk-go-v2/credentials v1.17.54
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.52 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.28 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.9 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.73.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.11 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.9 // indirect
github.com/aws/smithy-go v1.22.1 // indirect
github.com/aws/aws-sdk-go-v2 v1.36.0
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 // indirect
github.com/aws/aws-sdk-go-v2/config v1.29.5
github.com/aws/aws-sdk-go-v2/credentials v1.17.58
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.58 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.75.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 // indirect
github.com/aws/smithy-go v1.22.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudflare/cloudflare-go v0.114.0 // indirect
@ -152,7 +151,6 @@ require (
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/goccy/go-json v0.10.4 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
@ -160,11 +158,9 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
@ -175,31 +171,31 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1084 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
go.opencensus.io v0.24.0 // indirect
gocloud.dev v0.40.0 // indirect
golang.org/x/image v0.23.0 // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/oauth2 v0.25.0 // indirect
golang.org/x/sync v0.10.0
golang.org/x/sys v0.29.0 // indirect
golang.org/x/oauth2 v0.26.0 // indirect
golang.org/x/sync v0.11.0
golang.org/x/sys v0.30.0 // indirect
golang.org/x/term v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.9.0
golang.org/x/tools v0.29.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/api v0.217.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/grpc v1.69.4 // indirect
google.golang.org/protobuf v1.36.3 // indirect
google.golang.org/api v0.219.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489 // indirect
google.golang.org/grpc v1.70.0 // indirect
google.golang.org/protobuf v1.36.4 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.61.9 // indirect
modernc.org/libc v1.61.11 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.2 // indirect
modernc.org/sqlite v1.34.5 // indirect

156
go.sum
View File

@ -1,5 +1,5 @@
cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo=
cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
cel.dev/expr v0.19.0 h1:lXuo+nDhpyJSpWxpPVi5cPUwzKb+dsdOiw6IreM5yt0=
cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@ -49,8 +49,6 @@ cloud.google.com/go/storage v1.47.0/go.mod h1:Ks0vP374w0PW6jOUameJbapbQKXqkjGd/O
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
@ -90,8 +88,6 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapp
github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/sarama v1.30.1/go.mod h1:hGgx05L/DiW8XYBXeJdKIN6V2QUy2H6JqME5VT1NLRw=
@ -212,52 +208,52 @@ github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
github.com/aws/aws-sdk-go-v2 v1.33.0 h1:Evgm4DI9imD81V0WwD+TN4DCwjUMdc94TrduMLbgZJs=
github.com/aws/aws-sdk-go-v2 v1.33.0/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc=
github.com/aws/aws-sdk-go-v2/config v1.29.1 h1:JZhGawAyZ/EuJeBtbQYnaoftczcb2drR2Iq36Wgz4sQ=
github.com/aws/aws-sdk-go-v2/config v1.29.1/go.mod h1:7bR2YD5euaxBhzt2y/oDkt3uNRb6tjFp98GlTFueRwk=
github.com/aws/aws-sdk-go-v2/credentials v1.17.54 h1:4UmqeOqJPvdvASZWrKlhzpRahAulBfyTJQUaYy4+hEI=
github.com/aws/aws-sdk-go-v2/credentials v1.17.54/go.mod h1:RTdfo0P0hbbTxIhmQrOsC/PquBZGabEPnCaxxKRPSnI=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 h1:5grmdTdMsovn9kPZPI23Hhvp0ZyNm5cRO+IZFIYiAfw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24/go.mod h1:zqi7TVKTswH3Ozq28PkmBmgzG1tona7mo9G2IJg4Cis=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.52 h1:6kI83R98XOnnyzHv9g9KTYXFawMyeQq8NeEERWMAwJk=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.52/go.mod h1:Juj7unpf3CIrWpEyJZhRJ6rJl9IYX7Hd8HOlwaZq/LE=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28 h1:igORFSiH3bfq4lxKFkTSYDhJEUCYo6C8VKiWJjYwQuQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28/go.mod h1:3So8EA/aAYm36L7XIvCVwLa0s5N0P7o2b1oqnx/2R4g=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28 h1:1mOW9zAUMhTSrMDssEHS/ajx8JcAj/IcftzcmNlmVLI=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28/go.mod h1:kGlXVIWDfvt2Ox5zEaNglmq0hXPHgQFNMix33Tw22jA=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.28 h1:7kpeALOUeThs2kEjlAxlADAVfxKmkYAedlpZ3kdoSJ4=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.28/go.mod h1:pyaOYEdp1MJWgtXLy6q80r3DhsVdOIOZNB9hdTcJIvI=
github.com/aws/aws-sdk-go-v2 v1.36.0 h1:b1wM5CcE65Ujwn565qcwgtOTT1aT4ADOHHgglKjG7fk=
github.com/aws/aws-sdk-go-v2 v1.36.0/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 h1:zAxi9p3wsZMIaVCdoiQp2uZ9k1LsZvmAnoTBeZPXom0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8/go.mod h1:3XkePX5dSaxveLAYY7nsbsZZrKxCyEuE5pM4ziFxyGg=
github.com/aws/aws-sdk-go-v2/config v1.29.5 h1:4lS2IB+wwkj5J43Tq/AwvnscBerBJtQQ6YS7puzCI1k=
github.com/aws/aws-sdk-go-v2/config v1.29.5/go.mod h1:SNzldMlDVbN6nWxM7XsUiNXPSa1LWlqiXtvh/1PrJGg=
github.com/aws/aws-sdk-go-v2/credentials v1.17.58 h1:/d7FUpAPU8Lf2KUdjniQvfNdlMID0Sd9pS23FJ3SS9Y=
github.com/aws/aws-sdk-go-v2/credentials v1.17.58/go.mod h1:aVYW33Ow10CyMQGFgC0ptMRIqJWvJ4nxZb0sUiuQT/A=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.58 h1:/BsEGAyMai+KdXS+CMHlLhB5miAO19wOqE6tj8azWPM=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.58/go.mod h1:KHM3lfl/sAJBCoLI1Lsg5w4SD2VDYWwQi7vxbKhw7TI=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 h1:lWm9ucLSRFiI4dQQafLrEOmEDGry3Swrz0BIRdiHJqQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31/go.mod h1:Huu6GG0YTfbPphQkDSo4dEGmQRTKb9k9G7RdtyQWxuI=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 h1:ACxDklUKKXb48+eg5ROZXi1vDgfMyfIA/WyvqHcHI0o=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31/go.mod h1:yadnfsDwqXeVaohbGc/RaD287PuyRw2wugkh5ZL2J6k=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31 h1:8IwBjuLdqIO1dGB+dZ9zJEl8wzY3bVYxcs0Xyu/Lsc0=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31/go.mod h1:8tMBcuVjL4kP/ECEIWTCWtwV2kj6+ouEKl4cqR4iWLw=
github.com/aws/aws-sdk-go-v2/service/acm v1.30.13 h1:aPCPsgDxQqOS3zPJKYJQVh02q8stjSQ1haHaUucCAUM=
github.com/aws/aws-sdk-go-v2/service/acm v1.30.13/go.mod h1:3pfuOCVLzWu3aiavTB9bOIdZpVadNYt6fyZdp+fDOSU=
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.44.5 h1:oBLlEuSL5G9W8M4GtEVdNi+xsQP+9lphVkbYf38Isgs=
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.44.5/go.mod h1:H/t3dGwvHy2WJ+ZwyDBWva7ttsoxSxt5qC1OMcc0iJ0=
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.2 h1:e6um6+DWYQP1XCa+E9YVtG/9v1qk5lyAOelMOVwSyO8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.2/go.mod h1:dIW8puxSbYLSPv/ju0d9A3CpwXdtqvJtYKDMVmPLOWE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9 h1:TQmKDyETFGiXVhZfQ/I0cCFziqqX58pi4tKJGYGFSz0=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9/go.mod h1:HVLPK2iHQBUx7HfZeOQSEu3v2ubZaAY2YPbAm5/WUyY=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.9 h1:2aInXbh02XsbO0KobPGMNXyv2QP73VDKsWPNJARj/+4=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.9/go.mod h1:dgXS1i+HgWnYkPXqNoPIPKeUsUUYHaUbThC90aDnNiE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5 h1:siiQ+jummya9OLPDEyHVb2dLW4aOMe22FGDd0sAfuSw=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5/go.mod h1:iHVx2J9pWzITdP5MJY6qWfG34TfD9EA+Qi3eV6qQCXw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 h1:O+8vD2rGjfihBewr5bT+QUfYUHIxCVgG61LHoT59shM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12/go.mod h1:usVdWJaosa66NMvmCrr08NcWDBRv4E6+YFG2pUdw1Lk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12 h1:tkVNm99nkJnFo1H9IIQb5QkCiPcvCDn3Pos+IeTbGRA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12/go.mod h1:dIVlquSPUMqEJtx2/W17SM2SuESRaVEhEV9alcMqxjw=
github.com/aws/aws-sdk-go-v2/service/route53 v1.48.1 h1:njgAP7Rtt4DGdTGFPhJ4gaZXCD1CDj/SZDa5W4ZgSTs=
github.com/aws/aws-sdk-go-v2/service/route53 v1.48.1/go.mod h1:TN4PcCL0lvqmYcv+AV8iZFC4Sd0FM06QDaoBXrFEftU=
github.com/aws/aws-sdk-go-v2/service/s3 v1.73.2 h1:F3h8VYq9ZLBXYurmwrT8W0SPhgCcU0q+0WZJfT1dFt0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.73.2/go.mod h1:jGJ/v7FIi7Ys9t54tmEFnrxuaWeJLpwNgKp2DXAVhOU=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.11 h1:kuIyu4fTT38Kj7YCC7ouNbVZSSpqkZ+LzIfhCr6Dg+I=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.11/go.mod h1:Ro744S4fKiCCuZECXgOi760TiYylUM8ZBf6OGiZzJtY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10 h1:l+dgv/64iVlQ3WsBbnn+JSbkj01jIi+SM0wYsj3y/hY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10/go.mod h1:Fzsj6lZEb8AkTE5S68OhcbBqeWPsR8RnGuKPr8Todl8=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.9 h1:BRVDbewN6VZcwr+FBOszDKvYeXY1kJ+GGMCcpghlw0U=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.9/go.mod h1:f6vjfZER1M17Fokn0IzssOTMT2N8ZSq+7jnNF0tArvw=
github.com/aws/aws-sdk-go-v2/service/s3 v1.75.3 h1:JBod0SnNqcWQ0+uAyzeRFG1zCHotW8DukumYYyNy0zo=
github.com/aws/aws-sdk-go-v2/service/s3 v1.75.3/go.mod h1:FHSHmyEUkzRbaFFqqm6bkLAOQHgqhsLmfCahvCBMiyA=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 h1:3LXNnmtH3TURctC23hnC0p/39Q5gre3FI7BNOiDcVWc=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.13/go.mod h1:7Yn+p66q/jt38qMoVfNvjbm3D89mGBnkwDcijgtih8w=
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/baidubce/bce-sdk-go v0.9.214 h1:bsVfwMh/emI6vreEveUEq9xAr6xtHLycTAGy2K7kvKM=
github.com/baidubce/bce-sdk-go v0.9.214/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@ -305,8 +301,6 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -431,8 +425,6 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
@ -560,8 +552,6 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.132 h1:5LqzrJa8LADcY0sDEdV35e8nbwI7RoUQEt+KXWvWoY0=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.132/go.mod h1:JWz2ujO9X3oU5wb6kXp+DpR2UuDj2SldDbX8T0FSuhI=
@ -601,8 +591,6 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@ -634,7 +622,6 @@ github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjS
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@ -649,9 +636,6 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
@ -739,8 +723,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU=
github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
github.com/pocketbase/pocketbase v0.24.4 h1:kw/c23HccoxMV/19U9QlDcvNJgQ66vlUrxGQDZicWKM=
github.com/pocketbase/pocketbase v0.24.4/go.mod h1:EfXV/8RUY76jA6g1RPNHjOuW7wTd2bz0QlvAI/RU8YY=
github.com/pocketbase/pocketbase v0.25.0 h1:/4YQq1hd0muvhzbERyUTVNh88N0BCj5diqK0jtLN6k8=
github.com/pocketbase/pocketbase v0.25.0/go.mod h1:tOtOv7f3vJhAiyUluIwV9JPuKeknZRQ9F6uJE3W/ntI=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/povsister/scp v0.0.0-20240802064259-28781e87b246 h1:c4D8BPWLOxxdaxQLfLKQXH2YXY/E9yo3jrDSL54XrTw=
@ -805,8 +789,9 @@ github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
@ -964,14 +949,14 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -1056,8 +1041,8 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -1071,8 +1056,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -1147,8 +1132,8 @@ golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -1172,7 +1157,6 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
@ -1180,8 +1164,8 @@ golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -1269,8 +1253,8 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.217.0 h1:GYrUtD289o4zl1AhiTZL0jvQGa2RDLyC+kX1N/lfGOU=
google.golang.org/api v0.217.0/go.mod h1:qMc2E8cBAbQlRypBTBWHklNJlaZZJBwDv81B1Iu8oSI=
google.golang.org/api v0.219.0 h1:nnKIvxKs/06jWawp2liznTBnMRQBEPpGo7I+oEypTX0=
google.golang.org/api v0.219.0/go.mod h1:K6OmjGm+NtLrIkHxv1U3a0qIf/0JOvAHd5O/6AoyKYE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -1313,8 +1297,8 @@ google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489 h1:5bKytslY8ViY0Cj/ewmRtrWHW64bNF03cAatUUFCdFI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@ -1332,8 +1316,8 @@ google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -1349,8 +1333,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -1408,15 +1392,15 @@ k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJ
k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.23.13 h1:PFiaemQwE/jdwi8XEHyEV+qYWoIuikLP3T4rvDeJb00=
modernc.org/ccgo/v4 v4.23.13/go.mod h1:vdN4h2WR5aEoNondUx26K7G8X+nuBscYnAEWSRmN2/0=
modernc.org/ccgo/v4 v4.23.15 h1:wFDan71KnYqeHz4eF63vmGE6Q6Pc0PUGDpP0PRMYjDc=
modernc.org/ccgo/v4 v4.23.15/go.mod h1:nJX30dks/IWuBOnVa7VRii9Me4/9TZ1SC9GNtmARTy0=
modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.6.1 h1:+Qf6xdG8l7B27TQ8D8lw/iFMUj1RXRBOuMUWziJOsk8=
modernc.org/gc/v2 v2.6.1/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.61.9 h1:PLSBXVkifXGELtJ5BOnBUyAHr7lsatNwFU/RRo4kfJM=
modernc.org/libc v1.61.9/go.mod h1:61xrnzk/aR8gr5bR7Uj/lLFLuXu2/zMpIjcry63Eumk=
modernc.org/gc/v2 v2.6.2 h1:YBXi5Kqp6aCK3fIxwKQ3/fErvawVKwjOLItxj1brGds=
modernc.org/gc/v2 v2.6.2/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.61.11 h1:6sZG8uB6EMMG7iTLPTndi8jyTdgAQNIeLGjCFICACZw=
modernc.org/libc v1.61.11/go.mod h1:HHX+srFdn839oaJRd0W8hBM3eg+mieyZCAjWwB08/nM=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=

View File

@ -1,6 +1,7 @@
package applicant
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
@ -110,14 +111,11 @@ func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderCon
Kid: sslProviderConfig.Config.GoogleTrustServices.EabKid,
HmacEncoded: sslProviderConfig.Config.GoogleTrustServices.EabHmacKey,
})
case sslProviderLetsEncrypt, sslProviderLetsEncryptStaging:
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
default:
err = fmt.Errorf("unsupported ssl provider: %s", sslProviderConfig.Provider)
}
if err != nil {
return nil, err
}
@ -129,7 +127,12 @@ func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderCon
return resp.Resource, nil
}
if err := repo.Save(sslProviderConfig.Provider, user.GetEmail(), user.getPrivateKeyPEM(), reg); err != nil {
if _, err := repo.Save(context.Background(), &domain.AcmeAccount{
CA: sslProviderConfig.Provider,
Email: user.GetEmail(),
Key: user.getPrivateKeyPEM(),
Resource: reg,
}); err != nil {
return nil, fmt.Errorf("failed to save registration: %w", err)
}

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"
@ -30,18 +30,18 @@ type certificateRepository interface {
}
type CertificateService struct {
repo certificateRepository
certRepo certificateRepository
}
func NewCertificateService(repo certificateRepository) *CertificateService {
func NewCertificateService(certRepo certificateRepository) *CertificateService {
return &CertificateService{
repo: repo,
certRepo: certRepo,
}
}
func (s *CertificateService) InitSchedule(ctx context.Context) error {
app.GetScheduler().MustAdd("certificateExpireSoonNotify", "0 0 * * *", func() {
certs, err := s.repo.ListExpireSoon(context.Background())
certs, err := s.certRepo.ListExpireSoon(context.Background())
if err != nil {
app.GetLogger().Error("failed to get certificates which expire soon", "err", err)
return
@ -59,8 +59,8 @@ func (s *CertificateService) InitSchedule(ctx context.Context) error {
return nil
}
func (s *CertificateService) ArchiveFile(ctx context.Context, req *dtos.CertificateArchiveFileReq) ([]byte, error) {
certificate, err := s.repo.GetById(ctx, req.CertificateId)
func (s *CertificateService) ArchiveFile(ctx context.Context, req *dtos.CertificateArchiveFileReq) (*dtos.CertificateArchiveFileResp, error) {
certificate, err := s.certRepo.GetById(ctx, req.CertificateId)
if err != nil {
return nil, err
}
@ -69,6 +69,10 @@ func (s *CertificateService) ArchiveFile(ctx context.Context, req *dtos.Certific
zipWriter := zip.NewWriter(&buf)
defer zipWriter.Close()
resp := &dtos.CertificateArchiveFileResp{
FileFormat: "zip",
}
switch strings.ToUpper(req.Format) {
case "", "PEM":
{
@ -97,7 +101,8 @@ func (s *CertificateService) ArchiveFile(ctx context.Context, req *dtos.Certific
return nil, err
}
return buf.Bytes(), nil
resp.FileBytes = buf.Bytes()
return resp, nil
}
case "PFX":
@ -134,7 +139,8 @@ func (s *CertificateService) ArchiveFile(ctx context.Context, req *dtos.Certific
return nil, err
}
return buf.Bytes(), nil
resp.FileBytes = buf.Bytes()
return resp, nil
}
case "JKS":
@ -171,7 +177,8 @@ func (s *CertificateService) ArchiveFile(ctx context.Context, req *dtos.Certific
return nil, err
}
return buf.Bytes(), nil
resp.FileBytes = buf.Bytes()
return resp, nil
}
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) {
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 {
return nil, err
}
if time.Now().After(info.NotAfter) {
return nil, errors.New("证书已过期")
}
return &dtos.CertificateValidateCertificateResp{
Domains: strings.Join(info.DNSNames, ";"),
return &dtos.CertificateValidatePrivateKeyResp{
IsValid: true,
}, 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 {
Subject 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:
{
access := domain.AccessConfigForAliyun{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err)
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
switch options.Provider {
@ -146,8 +146,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeAWSCloudFront:
{
access := domain.AccessConfigForAWS{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err)
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
switch options.Provider {
@ -168,8 +168,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeBaiduCloudCDN:
{
access := domain.AccessConfigForBaiduCloud{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err)
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
switch options.Provider {
@ -189,8 +189,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeBytePlusCDN:
{
access := domain.AccessConfigForBytePlus{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err)
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
switch options.Provider {
@ -210,8 +210,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeDogeCloudCDN:
{
access := domain.AccessConfigForDogeCloud{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err)
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
deployer, err := providerDogeCDN.NewWithLogger(&providerDogeCDN.DogeCloudCDNDeployerConfig{
@ -225,8 +225,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeEdgioApplications:
{
access := domain.AccessConfigForEdgio{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err)
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
deployer, err := providerEdgioApplications.NewWithLogger(&providerEdgioApplications.EdgioApplicationsDeployerConfig{
@ -240,8 +240,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeHuaweiCloudCDN, domain.DeployProviderTypeHuaweiCloudELB:
{
access := domain.AccessConfigForHuaweiCloud{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err)
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
switch options.Provider {
@ -291,8 +291,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeKubernetesSecret:
{
access := domain.AccessConfigForKubernetes{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err)
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
deployer, err := providerK8sSecret.NewWithLogger(&providerK8sSecret.K8sSecretDeployerConfig{
@ -309,8 +309,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeQiniuCDN, domain.DeployProviderTypeQiniuPili:
{
access := domain.AccessConfigForQiniu{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err)
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
switch options.Provider {
@ -339,8 +339,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeSSH:
{
access := domain.AccessConfigForSSH{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err)
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
deployer, err := providerSSH.NewWithLogger(&providerSSH.SshDeployerConfig{
@ -367,8 +367,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeTencentCloudCDN, domain.DeployProviderTypeTencentCloudCLB, domain.DeployProviderTypeTencentCloudCOS, domain.DeployProviderTypeTencentCloudCSS, domain.DeployProviderTypeTencentCloudECDN, domain.DeployProviderTypeTencentCloudEO:
{
access := domain.AccessConfigForTencentCloud{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err)
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
switch options.Provider {
@ -435,8 +435,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeUCloudUCDN, domain.DeployProviderTypeUCloudUS3:
{
access := domain.AccessConfigForUCloud{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err)
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
switch options.Provider {
@ -468,8 +468,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeVolcEngineCDN, domain.DeployProviderTypeVolcEngineCLB, domain.DeployProviderTypeVolcEngineDCDN, domain.DeployProviderTypeVolcEngineLive, domain.DeployProviderTypeVolcEngineTOS:
{
access := domain.AccessConfigForVolcEngine{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err)
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
switch options.Provider {
@ -525,8 +525,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
case domain.DeployProviderTypeWebhook:
{
access := domain.AccessConfigForWebhook{}
if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err)
if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
deployer, err := providerWebhook.NewWithLogger(&providerWebhook.WebhookDeployerConfig{

View File

@ -1,24 +1,77 @@
package domain
import "time"
import (
"crypto/x509"
"strings"
"time"
"github.com/usual2970/certimate/internal/pkg/utils/certs"
)
const CollectionNameCertificate = "certificate"
type Certificate struct {
Meta
Source CertificateSourceType `json:"source" db:"source"`
SubjectAltNames string `json:"subjectAltNames" db:"subjectAltNames"`
Certificate string `json:"certificate" db:"certificate"`
PrivateKey string `json:"privateKey" db:"privateKey"`
IssuerCertificate string `json:"issuerCertificate" db:"issuerCertificate"`
EffectAt time.Time `json:"effectAt" db:"effectAt"`
ExpireAt time.Time `json:"expireAt" db:"expireAt"`
ACMECertUrl string `json:"acmeCertUrl" db:"acmeCertUrl"`
ACMECertStableUrl string `json:"acmeCertStableUrl" db:"acmeCertStableUrl"`
WorkflowId string `json:"workflowId" db:"workflowId"`
WorkflowNodeId string `json:"workflowNodeId" db:"workflowNodeId"`
WorkflowOutputId string `json:"workflowOutputId" db:"workflowOutputId"`
DeletedAt *time.Time `json:"deleted" db:"deleted"`
Source CertificateSourceType `json:"source" db:"source"`
SubjectAltNames string `json:"subjectAltNames" db:"subjectAltNames"`
SerialNumber string `json:"serialNumber" db:"serialNumber"`
Certificate string `json:"certificate" db:"certificate"`
PrivateKey string `json:"privateKey" db:"privateKey"`
Issuer string `json:"issuer" db:"issuer"`
IssuerCertificate string `json:"issuerCertificate" db:"issuerCertificate"`
KeyAlgorithm CertificateKeyAlgorithmType `json:"keyAlgorithm" db:"keyAlgorithm"`
EffectAt time.Time `json:"effectAt" db:"effectAt"`
ExpireAt time.Time `json:"expireAt" db:"expireAt"`
ACMEAccountUrl string `json:"acmeAccountUrl" db:"acmeAccountUrl"`
ACMECertUrl string `json:"acmeCertUrl" db:"acmeCertUrl"`
ACMECertStableUrl string `json:"acmeCertStableUrl" db:"acmeCertStableUrl"`
WorkflowId string `json:"workflowId" db:"workflowId"`
WorkflowNodeId string `json:"workflowNodeId" db:"workflowNodeId"`
WorkflowRunId string `json:"workflowRunId" db:"workflowRunId"`
WorkflowOutputId string `json:"workflowOutputId" db:"workflowOutputId"`
DeletedAt *time.Time `json:"deleted" db:"deleted"`
}
func (c *Certificate) PopulateFromX509(certX509 *x509.Certificate) *Certificate {
c.SubjectAltNames = strings.Join(certX509.DNSNames, ";")
c.SerialNumber = strings.ToUpper(certX509.SerialNumber.Text(16))
c.Issuer = strings.Join(certX509.Issuer.Organization, ";")
c.EffectAt = certX509.NotBefore
c.ExpireAt = certX509.NotAfter
switch certX509.SignatureAlgorithm {
case x509.SHA256WithRSA, x509.SHA256WithRSAPSS:
c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA2048
case x509.SHA384WithRSA, x509.SHA384WithRSAPSS:
c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA3072
case x509.SHA512WithRSA, x509.SHA512WithRSAPSS:
c.KeyAlgorithm = CertificateKeyAlgorithmTypeRSA4096
case x509.ECDSAWithSHA256:
c.KeyAlgorithm = CertificateKeyAlgorithmTypeEC256
case x509.ECDSAWithSHA384:
c.KeyAlgorithm = CertificateKeyAlgorithmTypeEC384
case x509.ECDSAWithSHA512:
c.KeyAlgorithm = CertificateKeyAlgorithmTypeEC512
default:
c.KeyAlgorithm = CertificateKeyAlgorithmType("")
}
return c
}
func (c *Certificate) PopulateFromPEM(certPEM, privkeyPEM string) *Certificate {
c.Certificate = certPEM
c.PrivateKey = privkeyPEM
_, issuerCertPEM, _ := certs.ExtractCertificatesFromPEM(certPEM)
c.IssuerCertificate = issuerCertPEM
certX509, _ := certs.ParseCertificateFromPEM(certPEM)
if certX509 != nil {
c.PopulateFromX509(certX509)
}
return c
}
type CertificateSourceType string
@ -27,3 +80,15 @@ const (
CertificateSourceTypeWorkflow = CertificateSourceType("workflow")
CertificateSourceTypeUpload = CertificateSourceType("upload")
)
type CertificateKeyAlgorithmType string
const (
CertificateKeyAlgorithmTypeRSA2048 = CertificateKeyAlgorithmType("RSA2048")
CertificateKeyAlgorithmTypeRSA3072 = CertificateKeyAlgorithmType("RSA3072")
CertificateKeyAlgorithmTypeRSA4096 = CertificateKeyAlgorithmType("RSA4096")
CertificateKeyAlgorithmTypeRSA8192 = CertificateKeyAlgorithmType("RSA8192")
CertificateKeyAlgorithmTypeEC256 = CertificateKeyAlgorithmType("EC256")
CertificateKeyAlgorithmTypeEC384 = CertificateKeyAlgorithmType("EC384")
CertificateKeyAlgorithmTypeEC512 = CertificateKeyAlgorithmType("EC512")
)

View File

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

View File

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

View File

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

View File

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

View File

@ -31,17 +31,26 @@ const (
type WorkflowRunLog struct {
NodeId string `json:"nodeId"`
NodeName string `json:"nodeName"`
Records []WorkflowRunLogRecord `json:"records"`
Error string `json:"error"`
Outputs []WorkflowRunLogOutput `json:"outputs"`
}
type WorkflowRunLogOutput struct {
Time string `json:"time"`
Title string `json:"title"`
Content string `json:"content"`
Error string `json:"error"`
type WorkflowRunLogRecord struct {
Time string `json:"time"`
Level WorkflowRunLogLevel `json:"level"`
Content string `json:"content"`
Error string `json:"error"`
}
type WorkflowRunLogLevel string
const (
WorkflowRunLogLevelDebug WorkflowRunLogLevel = "DEBUG"
WorkflowRunLogLevelInfo WorkflowRunLogLevel = "INFO"
WorkflowRunLogLevelWarn WorkflowRunLogLevel = "WARN"
WorkflowRunLogLevelError WorkflowRunLogLevel = "ERROR"
)
type WorkflowRunLogs []WorkflowRunLog
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)
return err
}
return nil
}
func (d *DNSProvider) removeDNSRecord(domain, subDomain, value string) error {

View File

@ -2,13 +2,13 @@
import (
"context"
"encoding/pem"
"errors"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/logger"
"github.com/usual2970/certimate/internal/pkg/utils/certs"
edgsdk "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7"
edgsdkDtos "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7/dtos"
)
@ -57,7 +57,10 @@ func NewWithLogger(config *EdgioApplicationsDeployerConfig, logger logger.Logger
func (d *EdgioApplicationsDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 提取 Edgio 所需的服务端证书和中间证书内容
privateCertPem, intermediateCertPem := extractCertChains(certPem)
privateCertPem, intermediateCertPem, err := certs.ExtractCertificatesFromPEM(certPem)
if err != nil {
return nil, err
}
// 上传 TLS 证书
// REF: https://docs.edg.io/rest_api/#tag/tls-certs/operation/postConfigV01TlsCerts
@ -81,32 +84,3 @@ func createSdkClient(clientId, clientSecret string) (*edgsdk.EdgioClient, error)
client := edgsdk.NewEdgioClient(clientId, clientSecret, "", "")
return client, nil
}
func extractCertChains(certPem string) (primaryCertPem string, intermediateCertPem string) {
pemBlocks := make([]*pem.Block, 0)
pemData := []byte(certPem)
for {
block, rest := pem.Decode(pemData)
if block == nil {
break
}
pemBlocks = append(pemBlocks, block)
pemData = rest
}
primaryCertPem = ""
intermediateCertPem = ""
if len(pemBlocks) > 0 {
primaryCertPem = string(pem.EncodeToMemory(pemBlocks[0]))
}
if len(pemBlocks) > 1 {
for i := 1; i < len(pemBlocks); i++ {
intermediateCertPem += string(pem.EncodeToMemory(pemBlocks[i]))
}
}
return primaryCertPem, intermediateCertPem
}

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

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
uploadSslCertResp, err := u.sdkClient.UploadSslCert(certName, certX509.Subject.CommonName, certPem, privkeyPem)
uploadSslCertResp, err := u.sdkClient.UploadSslCert(context.TODO(), certName, certX509.Subject.CommonName, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.UploadSslCert'")
}

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 内容可能是包含多张证书的证书链,但只返回第一个证书(即服务器证书)。
//
// 入参:
// - certPem: 证书 PEM 内容。

View File

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

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

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
import (
"bytes"
"encoding/json"
"context"
"fmt"
"io"
"net/http"
"strings"
"github.com/qiniu/go-sdk/v7/auth"
"github.com/qiniu/go-sdk/v7/client"
)
const qiniuHost = "https://api.qiniu.com"
type Client struct {
mac *auth.Credentials
client *client.Client
}
func NewClient(mac *auth.Credentials) *Client {
if mac == nil {
mac = auth.Default()
}
return &Client{mac: mac}
client := client.DefaultClient
client.Transport = newTransport(mac, nil)
return &Client{client: &client}
}
func (c *Client) GetDomainInfo(domain string) (*GetDomainInfoResponse, error) {
respBytes, err := c.sendReq(http.MethodGet, fmt.Sprintf("domain/%s", domain), nil)
if err != nil {
func (c *Client) GetDomainInfo(ctx context.Context, domain string) (*GetDomainInfoResponse, error) {
resp := new(GetDomainInfoResponse)
if err := c.client.Call(ctx, resp, http.MethodGet, c.urlf("domain/%s", domain), nil); err != nil {
return nil, err
}
resp := &GetDomainInfoResponse{}
err = json.Unmarshal(respBytes, resp)
if err != nil {
return nil, err
}
if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 {
return nil, fmt.Errorf("code: %d, error: %s", *resp.Code, *resp.Error)
}
return resp, nil
}
func (c *Client) ModifyDomainHttpsConf(domain, certId string, forceHttps, http2Enable bool) (*ModifyDomainHttpsConfResponse, error) {
func (c *Client) ModifyDomainHttpsConf(ctx context.Context, domain string, certId string, forceHttps bool, http2Enable bool) (*ModifyDomainHttpsConfResponse, error) {
req := &ModifyDomainHttpsConfRequest{
DomainInfoHttpsData: DomainInfoHttpsData{
CertID: certId,
@ -50,30 +42,14 @@ func (c *Client) ModifyDomainHttpsConf(domain, certId string, forceHttps, http2E
Http2Enable: http2Enable,
},
}
reqBytes, err := json.Marshal(req)
if err != nil {
resp := new(ModifyDomainHttpsConfResponse)
if err := c.client.CallWithJson(ctx, resp, http.MethodPut, c.urlf("domain/%s/httpsconf", domain), nil, req); err != nil {
return nil, err
}
respBytes, err := c.sendReq(http.MethodPut, fmt.Sprintf("domain/%s/httpsconf", domain), bytes.NewReader(reqBytes))
if err != nil {
return nil, err
}
resp := &ModifyDomainHttpsConfResponse{}
err = json.Unmarshal(respBytes, resp)
if err != nil {
return nil, err
}
if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 {
return nil, fmt.Errorf("code: %d, error: %s", *resp.Code, *resp.Error)
}
return resp, nil
}
func (c *Client) EnableDomainHttps(domain, certId string, forceHttps, http2Enable bool) (*EnableDomainHttpsResponse, error) {
func (c *Client) EnableDomainHttps(ctx context.Context, domain string, certId string, forceHttps bool, http2Enable bool) (*EnableDomainHttpsResponse, error) {
req := &EnableDomainHttpsRequest{
DomainInfoHttpsData: DomainInfoHttpsData{
CertID: certId,
@ -81,83 +57,29 @@ func (c *Client) EnableDomainHttps(domain, certId string, forceHttps, http2Enabl
Http2Enable: http2Enable,
},
}
reqBytes, err := json.Marshal(req)
if err != nil {
resp := new(EnableDomainHttpsResponse)
if err := c.client.CallWithJson(ctx, resp, http.MethodPut, c.urlf("domain/%s/sslize", domain), nil, req); err != nil {
return nil, err
}
respBytes, err := c.sendReq(http.MethodPut, fmt.Sprintf("domain/%s/sslize", domain), bytes.NewReader(reqBytes))
if err != nil {
return nil, err
}
resp := &EnableDomainHttpsResponse{}
err = json.Unmarshal(respBytes, resp)
if err != nil {
return nil, err
}
if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 {
return nil, fmt.Errorf("code: %d, error: %s", *resp.Code, *resp.Error)
}
return resp, nil
}
func (c *Client) UploadSslCert(name, commonName, certificate, privateKey string) (*UploadSslCertResponse, error) {
func (c *Client) UploadSslCert(ctx context.Context, name string, commonName string, certificate string, privateKey string) (*UploadSslCertResponse, error) {
req := &UploadSslCertRequest{
Name: name,
CommonName: commonName,
Certificate: certificate,
PrivateKey: privateKey,
}
reqBytes, err := json.Marshal(req)
if err != nil {
resp := new(UploadSslCertResponse)
if err := c.client.CallWithJson(ctx, resp, http.MethodPost, c.urlf("sslcert"), nil, req); err != nil {
return nil, err
}
respBytes, err := c.sendReq(http.MethodPost, "sslcert", bytes.NewReader(reqBytes))
if err != nil {
return nil, err
}
resp := &UploadSslCertResponse{}
err = json.Unmarshal(respBytes, resp)
if err != nil {
return nil, err
}
if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 {
return nil, fmt.Errorf("qiniu api error, code: %d, error: %s", *resp.Code, *resp.Error)
}
return resp, nil
}
func (c *Client) sendReq(method string, path string, body io.Reader) ([]byte, error) {
func (c *Client) urlf(pathf string, pathargs ...any) string {
path := fmt.Sprintf(pathf, pathargs...)
path = strings.TrimPrefix(path, "/")
req, err := http.NewRequest(method, fmt.Sprintf("%s/%s", qiniuHost, path), body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", "application/json")
if err := c.mac.AddToken(auth.TokenQBox, req); err != nil {
return nil, err
}
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
r, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return r, nil
return qiniuHost + "/" + path
}

View File

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

View File

@ -1,6 +1,9 @@
package repository
import (
"context"
"database/sql"
"errors"
"fmt"
"github.com/go-acme/lego/v4/registration"
@ -48,18 +51,37 @@ func (r *AcmeAccountRepository) GetByCAAndEmail(ca, email string) (*domain.AcmeA
return r.castRecordToModel(record)
}
func (r *AcmeAccountRepository) Save(ca, email, key string, resource *registration.Resource) error {
func (r *AcmeAccountRepository) Save(ctx context.Context, acmeAccount *domain.AcmeAccount) (*domain.AcmeAccount, error) {
collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameAcmeAccount)
if err != nil {
return err
return acmeAccount, err
}
record := core.NewRecord(collection)
record.Set("ca", ca)
record.Set("email", email)
record.Set("key", key)
record.Set("resource", resource)
return app.GetApp().Save(record)
var record *core.Record
if acmeAccount.Id == "" {
record = core.NewRecord(collection)
} else {
record, err = app.GetApp().FindRecordById(collection, acmeAccount.Id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return acmeAccount, domain.ErrRecordNotFound
}
return acmeAccount, err
}
}
record.Set("ca", acmeAccount.CA)
record.Set("email", acmeAccount.Email)
record.Set("key", acmeAccount.Key)
record.Set("resource", acmeAccount.Resource)
if err := app.GetApp().Save(record); err != nil {
return acmeAccount, err
}
acmeAccount.Id = record.Id
acmeAccount.CreatedAt = record.GetDateTime("created").Time()
acmeAccount.UpdatedAt = record.GetDateTime("updated").Time()
return acmeAccount, nil
}
func (r *AcmeAccountRepository) castRecordToModel(record *core.Record) (*domain.AcmeAccount, error) {

View File

@ -79,6 +79,52 @@ func (r *CertificateRepository) GetByWorkflowNodeId(ctx context.Context, workflo
return r.castRecordToModel(records[0])
}
func (r *CertificateRepository) Save(ctx context.Context, certificate *domain.Certificate) (*domain.Certificate, error) {
collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameCertificate)
if err != nil {
return certificate, err
}
var record *core.Record
if certificate.Id == "" {
record = core.NewRecord(collection)
} else {
record, err = app.GetApp().FindRecordById(collection, certificate.Id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return certificate, domain.ErrRecordNotFound
}
return certificate, err
}
}
record.Set("source", string(certificate.Source))
record.Set("subjectAltNames", certificate.SubjectAltNames)
record.Set("serialNumber", certificate.SerialNumber)
record.Set("certificate", certificate.Certificate)
record.Set("privateKey", certificate.PrivateKey)
record.Set("issuer", certificate.Issuer)
record.Set("issuerCertificate", certificate.IssuerCertificate)
record.Set("keyAlgorithm", string(certificate.KeyAlgorithm))
record.Set("effectAt", certificate.EffectAt)
record.Set("expireAt", certificate.ExpireAt)
record.Set("acmeAccountUrl", certificate.ACMEAccountUrl)
record.Set("acmeCertUrl", certificate.ACMECertUrl)
record.Set("acmeCertStableUrl", certificate.ACMECertStableUrl)
record.Set("workflowId", certificate.WorkflowId)
record.Set("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) {
if record == 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")),
SubjectAltNames: record.GetString("subjectAltNames"),
SerialNumber: record.GetString("serialNumber"),
Certificate: record.GetString("certificate"),
PrivateKey: record.GetString("privateKey"),
Issuer: record.GetString("issuer"),
IssuerCertificate: record.GetString("issuerCertificate"),
KeyAlgorithm: domain.CertificateKeyAlgorithmType(record.GetString("keyAlgorithm")),
EffectAt: record.GetDateTime("effectAt").Time(),
ExpireAt: record.GetDateTime("expireAt").Time(),
ACMEAccountUrl: record.GetString("acmeAccountUrl"),
ACMECertUrl: record.GetString("acmeCertUrl"),
ACMECertStableUrl: record.GetString("acmeCertStableUrl"),
WorkflowId: record.GetString("workflowId"),
WorkflowRunId: record.GetString("workflowRunId"),
WorkflowNodeId: record.GetString("workflowNodeId"),
WorkflowOutputId: record.GetString("workflowOutputId"),
}

View File

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

View File

@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"errors"
"fmt"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
@ -17,13 +18,13 @@ func NewWorkflowOutputRepository() *WorkflowOutputRepository {
return &WorkflowOutputRepository{}
}
func (r *WorkflowOutputRepository) GetByNodeId(ctx context.Context, nodeId string) (*domain.WorkflowOutput, error) {
func (r *WorkflowOutputRepository) GetByNodeId(ctx context.Context, workflowNodeId string) (*domain.WorkflowOutput, error) {
records, err := app.GetApp().FindRecordsByFilter(
domain.CollectionNameWorkflowOutput,
"nodeId={:nodeId}",
"-created",
1, 0,
dbx.Params{"nodeId": nodeId},
dbx.Params{"nodeId": workflowNodeId},
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
@ -34,103 +35,128 @@ func (r *WorkflowOutputRepository) GetByNodeId(ctx context.Context, nodeId strin
if len(records) == 0 {
return nil, domain.ErrRecordNotFound
}
record := records[0]
return r.castRecordToModel(records[0])
}
func (r *WorkflowOutputRepository) Save(ctx context.Context, workflowOutput *domain.WorkflowOutput) (*domain.WorkflowOutput, error) {
record, err := r.saveRecord(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{}
if err := record.UnmarshalJSONField("node", node); err != nil {
return nil, errors.New("failed to unmarshal node")
return nil, err
}
outputs := make([]domain.WorkflowNodeIO, 0)
if err := record.UnmarshalJSONField("outputs", &outputs); err != nil {
return nil, errors.New("failed to unmarshal output")
return nil, err
}
rs := &domain.WorkflowOutput{
workflowOutput := &domain.WorkflowOutput{
Meta: domain.Meta{
Id: record.Id,
CreatedAt: record.GetDateTime("created").Time(),
UpdatedAt: record.GetDateTime("updated").Time(),
},
WorkflowId: record.GetString("workflowId"),
RunId: record.GetString("runId"),
NodeId: record.GetString("nodeId"),
Node: node,
Outputs: outputs,
Succeeded: record.GetBool("succeeded"),
}
return rs, nil
return workflowOutput, nil
}
// 保存节点输出
func (r *WorkflowOutputRepository) Save(ctx context.Context, output *domain.WorkflowOutput, certificate *domain.Certificate, cb func(id string) error) error {
var record *core.Record
var err error
func (r *WorkflowOutputRepository) saveRecord(workflowOutput *domain.WorkflowOutput) (*core.Record, error) {
collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflowOutput)
if err != nil {
return nil, err
}
if output.Id == "" {
collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflowOutput)
if err != nil {
return err
}
var record *core.Record
if workflowOutput.Id == "" {
record = core.NewRecord(collection)
} else {
record, err = app.GetApp().FindRecordById(domain.CollectionNameWorkflowOutput, output.Id)
record, err = app.GetApp().FindRecordById(collection, workflowOutput.Id)
if err != nil {
return err
return record, err
}
}
record.Set("workflowId", output.WorkflowId)
record.Set("nodeId", output.NodeId)
record.Set("node", output.Node)
record.Set("outputs", output.Outputs)
record.Set("succeeded", output.Succeeded)
record.Set("workflowId", workflowOutput.WorkflowId)
record.Set("runId", workflowOutput.RunId)
record.Set("nodeId", workflowOutput.NodeId)
record.Set("node", workflowOutput.Node)
record.Set("outputs", workflowOutput.Outputs)
record.Set("succeeded", workflowOutput.Succeeded)
if err := app.GetApp().Save(record); err != nil {
return err
return record, err
}
if cb != nil && certificate != nil {
if err := cb(record.Id); err != nil {
return err
}
certCollection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameCertificate)
if err != nil {
return err
}
certRecord := core.NewRecord(certCollection)
certRecord.Set("source", string(certificate.Source))
certRecord.Set("subjectAltNames", certificate.SubjectAltNames)
certRecord.Set("certificate", certificate.Certificate)
certRecord.Set("privateKey", certificate.PrivateKey)
certRecord.Set("issuerCertificate", certificate.IssuerCertificate)
certRecord.Set("effectAt", certificate.EffectAt)
certRecord.Set("expireAt", certificate.ExpireAt)
certRecord.Set("acmeCertUrl", certificate.ACMECertUrl)
certRecord.Set("acmeCertStableUrl", certificate.ACMECertStableUrl)
certRecord.Set("workflowId", certificate.WorkflowId)
certRecord.Set("workflowNodeId", certificate.WorkflowNodeId)
certRecord.Set("workflowOutputId", certificate.WorkflowOutputId)
if err := app.GetApp().Save(certRecord); err != nil {
return err
}
// 更新 certificate
for i, item := range output.Outputs {
if item.Name == string(domain.WorkflowNodeIONameCertificate) {
output.Outputs[i].Value = certRecord.Id
break
}
}
record.Set("outputs", output.Outputs)
if err := app.GetApp().Save(record); err != nil {
return err
}
}
return nil
return record, 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 {
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)
ValidatePrivateKey(ctx context.Context, req *dtos.CertificateValidatePrivateKeyReq) error
ValidatePrivateKey(ctx context.Context, req *dtos.CertificateValidatePrivateKeyReq) (*dtos.CertificateValidatePrivateKeyResp, error)
}
type CertificateHandler struct {
@ -38,10 +38,10 @@ func (handler *CertificateHandler) archiveFile(e *core.RequestEvent) error {
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)
} 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)
}
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)
} 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)
}
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)
} else {
return resp.Ok(e, nil)
return resp.Ok(e, res)
}
}

View File

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

View File

@ -27,13 +27,14 @@ func Register(router *router.Router[*core.RequestEvent]) {
certificateSvc = certificate.NewCertificateService(certificateRepo)
workflowRepo := repository.NewWorkflowRepository()
workflowSvc = workflow.NewWorkflowService(workflowRepo)
workflowRunRepo := repository.NewWorkflowRunRepository()
workflowSvc = workflow.NewWorkflowService(workflowRepo, workflowRunRepo)
statisticsRepo := repository.NewStatisticsRepository()
statisticsSvc = statistics.NewStatisticsService(statisticsRepo)
notifyRepo := repository.NewSettingsRepository()
notifySvc = notify.NewNotifyService(notifyRepo)
settingsRepo := repository.NewSettingsRepository()
notifySvc = notify.NewNotifyService(settingsRepo)
group := router.Group("/api")
group.Bind(apis.RequireSuperuserAuth())
@ -45,6 +46,6 @@ func Register(router *router.Router[*core.RequestEvent]) {
func Unregister() {
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
}
func NewCertificateScheduler(service certificateService) error {
func InitCertificateScheduler(service certificateService) error {
return service.InitSchedule(context.Background())
}

View File

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

View File

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

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() {
NewWorkflowService(repository.NewWorkflowRepository()).StartRun(ctx, &dtos.WorkflowStartRunReq{
workflowSrv := NewWorkflowService(repository.NewWorkflowRepository(), repository.NewWorkflowRunRepository())
workflowSrv.StartRun(ctx, &dtos.WorkflowStartRunReq{
WorkflowId: workflowId,
Trigger: domain.WorkflowTriggerTypeAuto,
RunTrigger: domain.WorkflowTriggerTypeAuto,
})
})
if err != nil {
app.GetLogger().Error("add cron job failed", "err", err)
return fmt.Errorf("add cron job failed: %w", err)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,90 +12,94 @@ import (
)
type uploadNode struct {
node *domain.WorkflowNode
outputRepo workflowOutputRepository
node *domain.WorkflowNode
*nodeLogger
certRepo certificateRepository
outputRepo workflowOutputRepository
}
func NewUploadNode(node *domain.WorkflowNode) *uploadNode {
return &uploadNode{
node: node,
nodeLogger: NewNodeLogger(node),
nodeLogger: newNodeLogger(node),
certRepo: repository.NewCertificateRepository(),
outputRepo: repository.NewWorkflowOutputRepository(),
}
}
// Run 上传证书节点执行
// 包含上传证书的工作流,理论上应该手动执行,如果每天定时执行,也只是重新保存一下
func (n *uploadNode) Run(ctx context.Context) error {
n.AddOutput(ctx,
n.node.Name,
"进入上传证书节点",
)
func (n *uploadNode) Process(ctx context.Context) error {
n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelInfo, "进入上传证书节点")
config := n.node.GetConfigForUpload()
nodeConfig := n.node.GetConfigForUpload()
// 检查证书是否过期
// 如果证书过期,则直接返回错误
certX509, err := certs.ParseCertificateFromPEM(config.Certificate)
if err != nil {
n.AddOutput(ctx,
n.node.Name,
"解析证书失败",
)
// 查询上次执行结果
lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id)
if err != nil && !domain.IsRecordNotFoundError(err) {
n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelError, "查询申请记录失败", err.Error())
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) {
n.AddOutput(ctx,
n.node.Name,
"证书已过期",
)
n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelWarn, "证书已过期")
return errors.New("certificate is expired")
}
// 生成证书实体
certificate := &domain.Certificate{
Source: domain.CertificateSourceTypeUpload,
SubjectAltNames: strings.Join(certX509.DNSNames, ";"),
Certificate: config.Certificate,
PrivateKey: config.PrivateKey,
EffectAt: certX509.NotBefore,
ExpireAt: certX509.NotAfter,
WorkflowId: getContextWorkflowId(ctx),
WorkflowNodeId: n.node.Id,
Source: domain.CertificateSourceTypeUpload,
}
certificate.PopulateFromPEM(nodeConfig.Certificate, nodeConfig.PrivateKey)
// 保存执行结果
// TODO: 先保持一个节点始终只有一个输出,后续增加版本控制
currentOutput := &domain.WorkflowOutput{
output := &domain.WorkflowOutput{
WorkflowId: getContextWorkflowId(ctx),
RunId: getContextWorkflowRunId(ctx),
NodeId: n.node.Id,
Node: n.node,
Succeeded: true,
Outputs: n.node.Outputs,
}
// 查询上次执行结果
lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id)
if err != nil && !domain.IsRecordNotFoundError(err) {
n.AddOutput(ctx, n.node.Name, "查询上传记录失败", err.Error())
if _, err := n.outputRepo.SaveWithCertificate(ctx, output, certificate); err != nil {
n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelError, "保存上传记录失败", err.Error())
return err
}
if lastOutput != nil {
currentOutput.Id = lastOutput.Id
}
if err := n.outputRepo.Save(ctx, currentOutput, certificate, func(id string) error {
if certificate != nil {
certificate.WorkflowOutputId = id
}
return nil
}); err != nil {
n.AddOutput(ctx, n.node.Name, "保存上传记录失败", err.Error())
return err
}
n.AddOutput(ctx, n.node.Name, "保存上传记录成功")
n.AppendLogRecord(ctx, domain.WorkflowRunLogLevelInfo, "保存上传记录成功")
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"
"errors"
"fmt"
"sync"
"time"
"github.com/usual2970/certimate/internal/app"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/domain/dtos"
processor "github.com/usual2970/certimate/internal/workflow/processor"
"github.com/usual2970/certimate/internal/workflow/dispatcher"
)
const defaultRoutines = 10
type workflowRunData struct {
Workflow *domain.Workflow
RunTrigger domain.WorkflowTriggerType
}
type workflowRepository interface {
ListEnabledAuto(ctx context.Context) ([]*domain.Workflow, error)
GetById(ctx context.Context, id string) (*domain.Workflow, error)
Save(ctx context.Context, workflow *domain.Workflow) (*domain.Workflow, error)
SaveRun(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error)
}
type workflowRunRepository interface {
GetById(ctx context.Context, id string) (*domain.WorkflowRun, error)
Save(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error)
}
type WorkflowService struct {
ch chan *workflowRunData
repo workflowRepository
wg sync.WaitGroup
cancel context.CancelFunc
dispatcher *dispatcher.WorkflowDispatcher
workflowRepo workflowRepository
workflowRunRepo workflowRunRepository
}
func NewWorkflowService(repo workflowRepository) *WorkflowService {
func NewWorkflowService(workflowRepo workflowRepository, workflowRunRepo workflowRunRepository) *WorkflowService {
srv := &WorkflowService{
repo: repo,
ch: make(chan *workflowRunData, 1),
dispatcher: dispatcher.GetSingletonDispatcher(workflowRepo, workflowRunRepo),
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
}
func (s *WorkflowService) InitSchedule(ctx context.Context) error {
workflows, err := s.repo.ListEnabledAuto(ctx)
workflows, err := s.workflowRepo.ListEnabledAuto(ctx)
if err != nil {
return err
}
scheduler := app.GetScheduler()
for _, workflow := range workflows {
var errs []error
err := scheduler.Add(fmt.Sprintf("workflow#%s", workflow.Id), workflow.TriggerCron, func() {
s.StartRun(ctx, &dtos.WorkflowStartRunReq{
WorkflowId: workflow.Id,
Trigger: domain.WorkflowTriggerTypeAuto,
RunTrigger: domain.WorkflowTriggerTypeAuto,
})
})
if err != nil {
app.GetLogger().Error("failed to add schedule", "err", err)
return err
errs = append(errs, 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 {
workflow, err := s.repo.GetById(ctx, req.WorkflowId)
workflow, err := s.workflowRepo.GetById(ctx, req.WorkflowId)
if err != nil {
app.GetLogger().Error("failed to get workflow", "id", req.WorkflowId, "err", err)
return err
}
if workflow.LastRunStatus == domain.WorkflowRunStatusTypeRunning {
return errors.New("workflow is running")
if workflow.LastRunStatus == domain.WorkflowRunStatusTypePending || workflow.LastRunStatus == domain.WorkflowRunStatusTypeRunning {
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{
WorkflowId: workflow.Id,
Status: domain.WorkflowRunStatusTypeRunning,
Trigger: runData.RunTrigger,
Status: domain.WorkflowRunStatusTypePending,
Trigger: req.RunTrigger,
StartedAt: time.Now(),
}
if resp, err := s.repo.SaveRun(ctx, run); err != nil {
if resp, err := s.workflowRunRepo.Save(ctx, run); err != nil {
return err
} else {
run = resp
}
processor := processor.NewWorkflowProcessor(workflow)
if runErr := processor.Run(ctx); runErr != nil {
run.Status = domain.WorkflowRunStatusTypeFailed
run.EndedAt = time.Now()
run.Logs = processor.GetRunLogs()
run.Error = runErr.Error()
if _, err := s.repo.SaveRun(ctx, run); err != nil {
app.GetLogger().Error("failed to save workflow run", "err", err)
}
return fmt.Errorf("failed to run workflow: %w", runErr)
}
run.EndedAt = time.Now()
run.Logs = processor.GetRunLogs()
run.Error = domain.WorkflowRunLogs(run.Logs).ErrorString()
if run.Error == "" {
run.Status = domain.WorkflowRunStatusTypeSucceeded
} else {
run.Status = domain.WorkflowRunStatusTypeFailed
}
if _, err := s.repo.SaveRun(ctx, run); err != nil {
app.GetLogger().Error("failed to save workflow run", "err", err)
return err
}
s.dispatcher.Dispatch(&dispatcher.WorkflowWorkerData{
WorkflowId: workflow.Id,
WorkflowContent: workflow.Content,
RunId: run.Id,
})
return nil
}
func (s *WorkflowService) CancelRun(ctx context.Context, req *dtos.WorkflowCancelRunReq) error {
workflow, err := s.workflowRepo.GetById(ctx, req.WorkflowId)
if err != nil {
return err
}
workflowRun, err := s.workflowRunRepo.GetById(ctx, req.RunId)
if err != nil {
return err
} else if workflowRun.WorkflowId != workflow.Id {
return errors.New("workflow run not found")
} else if workflowRun.Status != domain.WorkflowRunStatusTypePending && workflowRun.Status != domain.WorkflowRunStatusTypeRunning {
return errors.New("workflow run is not pending or running")
}
s.dispatcher.Cancel(workflowRun.Id)
return nil
}
func (s *WorkflowService) Shutdown(ctx context.Context) {
s.dispatcher.Shutdown()
}

View File

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

View File

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

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 { getPocketBase } from "@/repository/_pocketbase";
type ArchiveRespData = {
fileBytes: string;
};
export const archive = async (certificateId: string, format?: CertificateFormatType) => {
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",
headers: {
"Content-Type": "application/json",
@ -24,6 +28,7 @@ export const archive = async (certificateId: string, format?: CertificateFormatT
};
type ValidateCertificateResp = {
isValid: boolean;
domains: string;
};
@ -46,9 +51,13 @@ export const validateCertificate = async (certificate: string) => {
return resp;
};
type ValidatePrivateKeyResp = {
isValid: boolean;
};
export const validatePrivateKey = async (privateKey: string) => {
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",
headers: {
"Content-Type": "application/json",

View File

@ -22,7 +22,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
const handleDownloadClick = async (format: CertificateFormatType) => {
try {
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 blob = new Blob([u8arr], { type: "application/zip" });
saveAs(blob, `${data.id}-${data.subjectAltNames}.zip`);
@ -38,11 +38,27 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
<Form layout="vertical">
<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 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>
@ -59,7 +75,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
</CopyToClipboard>
</Tooltip>
</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>
@ -76,7 +92,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
</CopyToClipboard>
</Tooltip>
</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>

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 { Alert, Drawer, Typography } from "antd";
import dayjs from "dayjs";
import { Drawer } from "antd";
import Show from "@/components/Show";
import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun";
import { type WorkflowRunModel } from "@/domain/workflowRun";
import { useTriggerElement } from "@/hooks";
import WorkflowRunDetail from "./WorkflowRunDetail";
export type WorkflowRunDetailDrawerProps = {
data?: WorkflowRunModel;
loading?: boolean;
@ -16,8 +16,6 @@ export type WorkflowRunDetailDrawerProps = {
};
const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowRunDetailDrawerProps) => {
const { t } = useTranslation();
const [open, setOpen] = useControllableValue<boolean>(props, {
valuePropName: "open",
defaultValuePropName: "defaultOpen",
@ -30,37 +28,19 @@ const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowR
<>
{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!.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="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>
<WorkflowRunDetail data={data!} />
</Show>
</Drawer>
</>

View File

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

View File

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

View File

@ -56,6 +56,7 @@ const MULTIPLE_INPUT_DELIMITER = ";";
const initFormModel = (): ApplyNodeConfigFormFieldValues => {
return {
challengeType: "dns-01",
keyAlgorithm: "RSA2048",
skipBeforeExpiryDays: 20,
};
@ -74,6 +75,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
.every((e) => validDomainName(e, { allowWildcard: true }));
}, t("common.errmsg.domain_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")),
providerAccessId: z
.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")} />
</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]}>
<ApplyDNSProviderSelect
allowClear

View File

@ -86,7 +86,7 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
</Form.Item>
<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 className="mb-0">

View File

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

View File

@ -137,11 +137,11 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
return (
<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]}>
<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 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>
@ -151,7 +151,7 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
</Form.Item>
<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>

View File

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

View File

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

View File

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

View File

@ -15,11 +15,15 @@
"certificate.props.validity.expiration": "Expire on {{date}}",
"certificate.props.validity.filter.expire_soon": "Expire soon",
"certificate.props.validity.filter.expired": "Expired",
"certificate.props.brand": "Brand",
"certificate.props.source": "Source",
"certificate.props.source.workflow": "Workflow",
"certificate.props.source.upload": "Upload",
"certificate.props.certificate": "Certificate chain",
"certificate.props.private_key": "Private key",
"certificate.props.serial_number": "Serial number",
"certificate.props.key_algorithm": "Key algorithm",
"certificate.props.issuer": "Issuer",
"certificate.props.created_at": "Created at",
"certificate.props.updated_at": "Updated at"
}

View File

@ -18,7 +18,7 @@
"workflow_node.start.form.trigger_cron.label": "Cron expression",
"workflow_node.start.form.trigger_cron.placeholder": "Please enter cron expression",
"workflow_node.start.form.trigger_cron.errmsg.invalid": "Please enter a valid cron expression",
"workflow_node.start.form.trigger_cron.tooltip": "Time zone is based on the server.",
"workflow_node.start.form.trigger_cron.tooltip": "Exactly 5 space separated segments. Time zone is based on the server.",
"workflow_node.start.form.trigger_cron.extra": "Expected execution time for the last 5 times:",
"workflow_node.start.form.trigger_cron.guide": "Tips: If you have multiple workflows, it is recommended to set them to run at multiple times of the day instead of always running at specific times.<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.manual": "Manual",
"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.filter.expire_soon": "即将到期",
"certificate.props.validity.filter.expired": "已到期",
"certificate.props.brand": "证书品牌",
"certificate.props.source": "来源",
"certificate.props.source.workflow": "工作流",
"certificate.props.source.upload": "用户上传",
"certificate.props.certificate": "证书内容",
"certificate.props.private_key": "私钥内容",
"certificate.props.serial_number": "证书序列号",
"certificate.props.key_algorithm": "证书算法",
"certificate.props.issuer": "颁发者",
"certificate.props.created_at": "创建时间",
"certificate.props.updated_at": "更新时间"
}

View File

@ -18,7 +18,7 @@
"workflow_node.start.form.trigger_cron.label": "Cron 表达式",
"workflow_node.start.form.trigger_cron.placeholder": "请输入 Cron 表达式",
"workflow_node.start.form.trigger_cron.errmsg.invalid": "请输入正确的 Cron 表达式",
"workflow_node.start.form.trigger_cron.tooltip": "支持使用任意值(即 <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.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.manual": "手动执行",
"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 {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
line-height: 1.5;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@ -15,8 +10,7 @@
body {
margin: 0;
display: flex;
place-items: center;
padding: 0;
min-width: 320px;
min-height: 100vh;
}

View File

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

View File

@ -207,7 +207,7 @@ const AccessList = () => {
setPageSize(pageSize);
},
}}
rowKey={(record: AccessModel) => record.id}
rowKey={(record) => record.id}
scroll={{ x: "max(100%, 960px)" }}
/>
</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",
title: t("certificate.props.source"),
@ -250,7 +260,7 @@ const CertificateList = () => {
dataSource={tableData}
loading={loading}
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={{
current: page,
@ -266,7 +276,7 @@ const CertificateList = () => {
setPageSize(pageSize);
},
}}
rowKey={(record: CertificateModel) => record.id}
rowKey={(record) => record.id}
scroll={{ x: "max(100%, 960px)" }}
/>
</div>

View File

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

View File

@ -34,7 +34,7 @@ const SettingsPassword = () => {
onSubmit: async (values) => {
try {
await authWithPassword(getAuthStore().record!.email, values.oldPassword);
await saveAdmin({ password: values.newPassword });
await saveAdmin({ password: values.newPassword, passwordConfirm: values.confirmPassword });
messageApi.success(t("common.text.operation_succeeded"));

View File

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

View File

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

View File

@ -6,3 +6,11 @@ export const getPocketBase = () => {
pb = new PocketBase("/");
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 { type AccessModel } from "@/domain/access";
import { getPocketBase } from "./_pocketbase";
const COLLECTION_NAME = "access";
import { COLLECTION_NAME_ACCESS, getPocketBase } from "./_pocketbase";
export const list = async () => {
return await getPocketBase().collection(COLLECTION_NAME).getFullList<AccessModel>({
return await getPocketBase().collection(COLLECTION_NAME_ACCESS).getFullList<AccessModel>({
filter: "deleted=null",
sort: "-created",
requestKey: null,
@ -15,15 +13,15 @@ export const list = async () => {
export const save = async (record: MaybeModelRecord<AccessModel>) => {
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>) => {
await getPocketBase()
.collection(COLLECTION_NAME)
.collection(COLLECTION_NAME_ACCESS)
.update<AccessModel>(record.id!, { deleted: dayjs.utc().format("YYYY-MM-DD HH:mm:ss") });
return true;
};

View File

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

View File

@ -2,9 +2,7 @@ import dayjs from "dayjs";
import { type RecordListOptions } from "pocketbase";
import { type CertificateModel } from "@/domain/certificate";
import { getPocketBase } from "./_pocketbase";
const COLLECTION_NAME = "certificate";
import { COLLECTION_NAME_CERTIFICATE, getPocketBase } from "./_pocketbase";
export type ListCertificateRequest = {
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>) => {
await getPocketBase()
.collection(COLLECTION_NAME)
.collection(COLLECTION_NAME_CERTIFICATE)
.update<CertificateModel>(record.id!, { deleted: dayjs.utc().format("YYYY-MM-DD HH:mm:ss") });
return true;
};

View File

@ -1,13 +1,11 @@
import { ClientResponseError } from "pocketbase";
import { type SettingsModel, type SettingsNames } from "@/domain/settings";
import { getPocketBase } from "./_pocketbase";
const COLLECTION_NAME = "settings";
import { COLLECTION_NAME_SETTINGS, getPocketBase } from "./_pocketbase";
export const get = async <T extends NonNullable<unknown>>(name: SettingsNames) => {
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,
});
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>>) => {
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 WorkflowModel } from "@/domain/workflow";
import { getPocketBase } from "./_pocketbase";
const COLLECTION_NAME = "workflow";
import { COLLECTION_NAME_WORKFLOW, getPocketBase } from "./_pocketbase";
export type ListWorkflowRequest = {
page?: number;
@ -26,11 +24,11 @@ export const list = async (request: ListWorkflowRequest) => {
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) => {
return await getPocketBase().collection(COLLECTION_NAME).getOne<WorkflowModel>(id, {
return await getPocketBase().collection(COLLECTION_NAME_WORKFLOW).getOne<WorkflowModel>(id, {
requestKey: null,
});
};
@ -38,25 +36,21 @@ export const get = async (id: string) => {
export const save = async (record: MaybeModelRecord<WorkflowModel>) => {
if (record.id) {
return await getPocketBase()
.collection(COLLECTION_NAME)
.collection(COLLECTION_NAME_WORKFLOW)
.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>) => {
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) => {
const pb = getPocketBase();
return pb.collection("workflow").subscribe(id, cb);
return getPocketBase().collection(COLLECTION_NAME_WORKFLOW).subscribe(id, cb);
};
export const unsubscribe = async (id: string) => {
const pb = getPocketBase();
return pb.collection("workflow").unsubscribe(id);
return getPocketBase().collection(COLLECTION_NAME_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 = {
workflowId?: string;
@ -23,7 +23,7 @@ export const list = async (request: ListWorkflowRunsRequest) => {
}
return await getPocketBase()
.collection(COLLECTION_NAME)
.collection(COLLECTION_NAME_WORKFLOW_RUN)
.getList<WorkflowRunModel>(page, perPage, {
filter: getPocketBase().filter(filter, params),
sort: "-created",
@ -33,5 +33,13 @@ export const list = async (request: ListWorkflowRunsRequest) => {
};
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;
destroy(): void;
addNode: (node: WorkflowNode, preId: string) => void;
addNode: (node: WorkflowNode, previousNodeId: string) => void;
updateNode: (node: WorkflowNode) => void;
removeNode: (nodeId: string) => void;
addBranch: (branchId: string) => void;
removeBranch: (branchId: string, index: number) => void;
getWorkflowOuptutBeforeId: (id: string, type: string) => WorkflowNode[];
getWorkflowOuptutBeforeId: (nodeId: string, type: string) => WorkflowNode[];
};
export const useWorkflowStore = create<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";
const root = addNode(get().workflow.draft!, preId, node);
const root = addNode(get().workflow.draft!, previousNodeId, node);
const resp = await saveWorkflow({
id: get().workflow.id!,
draft: root,
@ -243,7 +243,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
});
},
getWorkflowOuptutBeforeId: (id: string, type: string) => {
return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, id, type);
getWorkflowOuptutBeforeId: (nodeId: string, type: string) => {
return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, nodeId, type);
},
}));

View File

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

View File

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