diff --git a/go.mod b/go.mod index 6cd082bf..cc7c0ff1 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0 github.com/pkg/sftp v1.13.7 github.com/pocketbase/dbx v1.11.0 - github.com/pocketbase/pocketbase v0.24.4 + github.com/pocketbase/pocketbase v0.25.0 github.com/povsister/scp v0.0.0-20240802064259-28781e87b246 github.com/qiniu/go-sdk/v7 v7.25.2 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1084 @@ -42,7 +42,7 @@ require ( github.com/volcengine/volc-sdk-golang v1.0.193 github.com/volcengine/volcengine-go-sdk v1.0.178 golang.org/x/crypto v0.32.0 - golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 + golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c k8s.io/api v0.32.1 k8s.io/apimachinery v0.32.1 k8s.io/client-go v0.32.1 @@ -108,7 +108,6 @@ require ( ) require ( - github.com/AlecAivazis/survey/v2 v2.3.7 // indirect github.com/BurntSushi/toml v1.4.0 // indirect github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect github.com/alibabacloud-go/dcdn-20180115/v3 v3.5.0 @@ -120,25 +119,25 @@ require ( github.com/aliyun/alibaba-cloud-sdk-go v1.63.83 // indirect github.com/aliyun/credentials-go v1.4.3 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/aws/aws-sdk-go-v2 v1.33.0 - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect - github.com/aws/aws-sdk-go-v2/config v1.29.1 - github.com/aws/aws-sdk-go-v2/credentials v1.17.54 - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 // indirect - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.52 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.28 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.2 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.9 // indirect - github.com/aws/aws-sdk-go-v2/service/s3 v1.73.2 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.24.11 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.33.9 // indirect - github.com/aws/smithy-go v1.22.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.36.0 + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 // indirect + github.com/aws/aws-sdk-go-v2/config v1.29.5 + github.com/aws/aws-sdk-go-v2/credentials v1.17.58 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.58 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.75.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 // indirect + github.com/aws/smithy-go v1.22.2 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect github.com/cloudflare/cloudflare-go v0.114.0 // indirect @@ -152,7 +151,6 @@ require ( github.com/go-jose/go-jose/v4 v4.0.4 // indirect github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect github.com/goccy/go-json v0.10.4 // indirect - github.com/golang-jwt/jwt/v4 v4.5.1 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect @@ -160,11 +158,9 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kr/fs v0.1.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/miekg/dns v1.1.62 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -175,31 +171,31 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/cobra v1.8.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/pflag v1.0.6 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.10.0 // indirect github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1084 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect go.opencensus.io v0.24.0 // indirect gocloud.dev v0.40.0 // indirect - golang.org/x/image v0.23.0 // indirect + golang.org/x/image v0.24.0 // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.34.0 // indirect - golang.org/x/oauth2 v0.25.0 // indirect - golang.org/x/sync v0.10.0 - golang.org/x/sys v0.29.0 // indirect + golang.org/x/oauth2 v0.26.0 // indirect + golang.org/x/sync v0.11.0 + golang.org/x/sys v0.30.0 // indirect golang.org/x/term v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/text v0.22.0 // indirect golang.org/x/time v0.9.0 golang.org/x/tools v0.29.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/api v0.217.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect - google.golang.org/grpc v1.69.4 // indirect - google.golang.org/protobuf v1.36.3 // indirect + google.golang.org/api v0.219.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489 // indirect + google.golang.org/grpc v1.70.0 // indirect + google.golang.org/protobuf v1.36.4 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.61.9 // indirect + modernc.org/libc v1.61.11 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.8.2 // indirect modernc.org/sqlite v1.34.5 // indirect diff --git a/go.sum b/go.sum index 7dde5dc8..199c33dd 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= -cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cel.dev/expr v0.19.0 h1:lXuo+nDhpyJSpWxpPVi5cPUwzKb+dsdOiw6IreM5yt0= +cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -49,8 +49,6 @@ cloud.google.com/go/storage v1.47.0/go.mod h1:Ks0vP374w0PW6jOUameJbapbQKXqkjGd/O dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= -github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= @@ -90,8 +88,6 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapp github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/sarama v1.30.1/go.mod h1:hGgx05L/DiW8XYBXeJdKIN6V2QUy2H6JqME5VT1NLRw= @@ -212,52 +208,52 @@ github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= -github.com/aws/aws-sdk-go-v2 v1.33.0 h1:Evgm4DI9imD81V0WwD+TN4DCwjUMdc94TrduMLbgZJs= -github.com/aws/aws-sdk-go-v2 v1.33.0/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc= -github.com/aws/aws-sdk-go-v2/config v1.29.1 h1:JZhGawAyZ/EuJeBtbQYnaoftczcb2drR2Iq36Wgz4sQ= -github.com/aws/aws-sdk-go-v2/config v1.29.1/go.mod h1:7bR2YD5euaxBhzt2y/oDkt3uNRb6tjFp98GlTFueRwk= -github.com/aws/aws-sdk-go-v2/credentials v1.17.54 h1:4UmqeOqJPvdvASZWrKlhzpRahAulBfyTJQUaYy4+hEI= -github.com/aws/aws-sdk-go-v2/credentials v1.17.54/go.mod h1:RTdfo0P0hbbTxIhmQrOsC/PquBZGabEPnCaxxKRPSnI= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 h1:5grmdTdMsovn9kPZPI23Hhvp0ZyNm5cRO+IZFIYiAfw= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24/go.mod h1:zqi7TVKTswH3Ozq28PkmBmgzG1tona7mo9G2IJg4Cis= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.52 h1:6kI83R98XOnnyzHv9g9KTYXFawMyeQq8NeEERWMAwJk= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.52/go.mod h1:Juj7unpf3CIrWpEyJZhRJ6rJl9IYX7Hd8HOlwaZq/LE= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28 h1:igORFSiH3bfq4lxKFkTSYDhJEUCYo6C8VKiWJjYwQuQ= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28/go.mod h1:3So8EA/aAYm36L7XIvCVwLa0s5N0P7o2b1oqnx/2R4g= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28 h1:1mOW9zAUMhTSrMDssEHS/ajx8JcAj/IcftzcmNlmVLI= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28/go.mod h1:kGlXVIWDfvt2Ox5zEaNglmq0hXPHgQFNMix33Tw22jA= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.28 h1:7kpeALOUeThs2kEjlAxlADAVfxKmkYAedlpZ3kdoSJ4= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.28/go.mod h1:pyaOYEdp1MJWgtXLy6q80r3DhsVdOIOZNB9hdTcJIvI= +github.com/aws/aws-sdk-go-v2 v1.36.0 h1:b1wM5CcE65Ujwn565qcwgtOTT1aT4ADOHHgglKjG7fk= +github.com/aws/aws-sdk-go-v2 v1.36.0/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 h1:zAxi9p3wsZMIaVCdoiQp2uZ9k1LsZvmAnoTBeZPXom0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8/go.mod h1:3XkePX5dSaxveLAYY7nsbsZZrKxCyEuE5pM4ziFxyGg= +github.com/aws/aws-sdk-go-v2/config v1.29.5 h1:4lS2IB+wwkj5J43Tq/AwvnscBerBJtQQ6YS7puzCI1k= +github.com/aws/aws-sdk-go-v2/config v1.29.5/go.mod h1:SNzldMlDVbN6nWxM7XsUiNXPSa1LWlqiXtvh/1PrJGg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.58 h1:/d7FUpAPU8Lf2KUdjniQvfNdlMID0Sd9pS23FJ3SS9Y= +github.com/aws/aws-sdk-go-v2/credentials v1.17.58/go.mod h1:aVYW33Ow10CyMQGFgC0ptMRIqJWvJ4nxZb0sUiuQT/A= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.58 h1:/BsEGAyMai+KdXS+CMHlLhB5miAO19wOqE6tj8azWPM= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.58/go.mod h1:KHM3lfl/sAJBCoLI1Lsg5w4SD2VDYWwQi7vxbKhw7TI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 h1:lWm9ucLSRFiI4dQQafLrEOmEDGry3Swrz0BIRdiHJqQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31/go.mod h1:Huu6GG0YTfbPphQkDSo4dEGmQRTKb9k9G7RdtyQWxuI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 h1:ACxDklUKKXb48+eg5ROZXi1vDgfMyfIA/WyvqHcHI0o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31/go.mod h1:yadnfsDwqXeVaohbGc/RaD287PuyRw2wugkh5ZL2J6k= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31 h1:8IwBjuLdqIO1dGB+dZ9zJEl8wzY3bVYxcs0Xyu/Lsc0= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31/go.mod h1:8tMBcuVjL4kP/ECEIWTCWtwV2kj6+ouEKl4cqR4iWLw= github.com/aws/aws-sdk-go-v2/service/acm v1.30.13 h1:aPCPsgDxQqOS3zPJKYJQVh02q8stjSQ1haHaUucCAUM= github.com/aws/aws-sdk-go-v2/service/acm v1.30.13/go.mod h1:3pfuOCVLzWu3aiavTB9bOIdZpVadNYt6fyZdp+fDOSU= github.com/aws/aws-sdk-go-v2/service/cloudfront v1.44.5 h1:oBLlEuSL5G9W8M4GtEVdNi+xsQP+9lphVkbYf38Isgs= github.com/aws/aws-sdk-go-v2/service/cloudfront v1.44.5/go.mod h1:H/t3dGwvHy2WJ+ZwyDBWva7ttsoxSxt5qC1OMcc0iJ0= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.2 h1:e6um6+DWYQP1XCa+E9YVtG/9v1qk5lyAOelMOVwSyO8= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.2/go.mod h1:dIW8puxSbYLSPv/ju0d9A3CpwXdtqvJtYKDMVmPLOWE= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9 h1:TQmKDyETFGiXVhZfQ/I0cCFziqqX58pi4tKJGYGFSz0= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9/go.mod h1:HVLPK2iHQBUx7HfZeOQSEu3v2ubZaAY2YPbAm5/WUyY= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.9 h1:2aInXbh02XsbO0KobPGMNXyv2QP73VDKsWPNJARj/+4= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.9/go.mod h1:dgXS1i+HgWnYkPXqNoPIPKeUsUUYHaUbThC90aDnNiE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5 h1:siiQ+jummya9OLPDEyHVb2dLW4aOMe22FGDd0sAfuSw= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5/go.mod h1:iHVx2J9pWzITdP5MJY6qWfG34TfD9EA+Qi3eV6qQCXw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 h1:O+8vD2rGjfihBewr5bT+QUfYUHIxCVgG61LHoT59shM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12/go.mod h1:usVdWJaosa66NMvmCrr08NcWDBRv4E6+YFG2pUdw1Lk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12 h1:tkVNm99nkJnFo1H9IIQb5QkCiPcvCDn3Pos+IeTbGRA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12/go.mod h1:dIVlquSPUMqEJtx2/W17SM2SuESRaVEhEV9alcMqxjw= github.com/aws/aws-sdk-go-v2/service/route53 v1.48.1 h1:njgAP7Rtt4DGdTGFPhJ4gaZXCD1CDj/SZDa5W4ZgSTs= github.com/aws/aws-sdk-go-v2/service/route53 v1.48.1/go.mod h1:TN4PcCL0lvqmYcv+AV8iZFC4Sd0FM06QDaoBXrFEftU= -github.com/aws/aws-sdk-go-v2/service/s3 v1.73.2 h1:F3h8VYq9ZLBXYurmwrT8W0SPhgCcU0q+0WZJfT1dFt0= -github.com/aws/aws-sdk-go-v2/service/s3 v1.73.2/go.mod h1:jGJ/v7FIi7Ys9t54tmEFnrxuaWeJLpwNgKp2DXAVhOU= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.11 h1:kuIyu4fTT38Kj7YCC7ouNbVZSSpqkZ+LzIfhCr6Dg+I= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.11/go.mod h1:Ro744S4fKiCCuZECXgOi760TiYylUM8ZBf6OGiZzJtY= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10 h1:l+dgv/64iVlQ3WsBbnn+JSbkj01jIi+SM0wYsj3y/hY= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10/go.mod h1:Fzsj6lZEb8AkTE5S68OhcbBqeWPsR8RnGuKPr8Todl8= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.9 h1:BRVDbewN6VZcwr+FBOszDKvYeXY1kJ+GGMCcpghlw0U= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.9/go.mod h1:f6vjfZER1M17Fokn0IzssOTMT2N8ZSq+7jnNF0tArvw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.75.3 h1:JBod0SnNqcWQ0+uAyzeRFG1zCHotW8DukumYYyNy0zo= +github.com/aws/aws-sdk-go-v2/service/s3 v1.75.3/go.mod h1:FHSHmyEUkzRbaFFqqm6bkLAOQHgqhsLmfCahvCBMiyA= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 h1:3LXNnmtH3TURctC23hnC0p/39Q5gre3FI7BNOiDcVWc= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.13/go.mod h1:7Yn+p66q/jt38qMoVfNvjbm3D89mGBnkwDcijgtih8w= github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= -github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= +github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/baidubce/bce-sdk-go v0.9.214 h1:bsVfwMh/emI6vreEveUEq9xAr6xtHLycTAGy2K7kvKM= github.com/baidubce/bce-sdk-go v0.9.214/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -305,8 +301,6 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= -github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -431,8 +425,6 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= -github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= @@ -560,8 +552,6 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= -github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= -github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.132 h1:5LqzrJa8LADcY0sDEdV35e8nbwI7RoUQEt+KXWvWoY0= github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.132/go.mod h1:JWz2ujO9X3oU5wb6kXp+DpR2UuDj2SldDbX8T0FSuhI= @@ -601,8 +591,6 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -634,7 +622,6 @@ github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjS github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -649,9 +636,6 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= -github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= @@ -739,8 +723,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU= github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs= -github.com/pocketbase/pocketbase v0.24.4 h1:kw/c23HccoxMV/19U9QlDcvNJgQ66vlUrxGQDZicWKM= -github.com/pocketbase/pocketbase v0.24.4/go.mod h1:EfXV/8RUY76jA6g1RPNHjOuW7wTd2bz0QlvAI/RU8YY= +github.com/pocketbase/pocketbase v0.25.0 h1:/4YQq1hd0muvhzbERyUTVNh88N0BCj5diqK0jtLN6k8= +github.com/pocketbase/pocketbase v0.25.0/go.mod h1:tOtOv7f3vJhAiyUluIwV9JPuKeknZRQ9F6uJE3W/ntI= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/povsister/scp v0.0.0-20240802064259-28781e87b246 h1:c4D8BPWLOxxdaxQLfLKQXH2YXY/E9yo3jrDSL54XrTw= @@ -805,8 +789,9 @@ github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= @@ -964,14 +949,14 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= -golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc= +golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= -golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= +golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= +golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1056,8 +1041,8 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1071,8 +1056,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1147,8 +1132,8 @@ golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -1172,7 +1157,6 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= @@ -1180,8 +1164,8 @@ golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1269,8 +1253,8 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.217.0 h1:GYrUtD289o4zl1AhiTZL0jvQGa2RDLyC+kX1N/lfGOU= -google.golang.org/api v0.217.0/go.mod h1:qMc2E8cBAbQlRypBTBWHklNJlaZZJBwDv81B1Iu8oSI= +google.golang.org/api v0.219.0 h1:nnKIvxKs/06jWawp2liznTBnMRQBEPpGo7I+oEypTX0= +google.golang.org/api v0.219.0/go.mod h1:K6OmjGm+NtLrIkHxv1U3a0qIf/0JOvAHd5O/6AoyKYE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1313,8 +1297,8 @@ google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489 h1:5bKytslY8ViY0Cj/ewmRtrWHW64bNF03cAatUUFCdFI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1332,8 +1316,8 @@ google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= -google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1349,8 +1333,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= +google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1408,15 +1392,15 @@ k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJ k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.23.13 h1:PFiaemQwE/jdwi8XEHyEV+qYWoIuikLP3T4rvDeJb00= -modernc.org/ccgo/v4 v4.23.13/go.mod h1:vdN4h2WR5aEoNondUx26K7G8X+nuBscYnAEWSRmN2/0= +modernc.org/ccgo/v4 v4.23.15 h1:wFDan71KnYqeHz4eF63vmGE6Q6Pc0PUGDpP0PRMYjDc= +modernc.org/ccgo/v4 v4.23.15/go.mod h1:nJX30dks/IWuBOnVa7VRii9Me4/9TZ1SC9GNtmARTy0= modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= -modernc.org/gc/v2 v2.6.1 h1:+Qf6xdG8l7B27TQ8D8lw/iFMUj1RXRBOuMUWziJOsk8= -modernc.org/gc/v2 v2.6.1/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/libc v1.61.9 h1:PLSBXVkifXGELtJ5BOnBUyAHr7lsatNwFU/RRo4kfJM= -modernc.org/libc v1.61.9/go.mod h1:61xrnzk/aR8gr5bR7Uj/lLFLuXu2/zMpIjcry63Eumk= +modernc.org/gc/v2 v2.6.2 h1:YBXi5Kqp6aCK3fIxwKQ3/fErvawVKwjOLItxj1brGds= +modernc.org/gc/v2 v2.6.2/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.61.11 h1:6sZG8uB6EMMG7iTLPTndi8jyTdgAQNIeLGjCFICACZw= +modernc.org/libc v1.61.11/go.mod h1:HHX+srFdn839oaJRd0W8hBM3eg+mieyZCAjWwB08/nM= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= diff --git a/internal/applicant/acme_user.go b/internal/applicant/acme_user.go index 1ab4c424..f8e80a03 100644 --- a/internal/applicant/acme_user.go +++ b/internal/applicant/acme_user.go @@ -1,6 +1,7 @@ package applicant import ( + "context" "crypto" "crypto/ecdsa" "crypto/elliptic" @@ -110,14 +111,11 @@ func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderCon Kid: sslProviderConfig.Config.GoogleTrustServices.EabKid, HmacEncoded: sslProviderConfig.Config.GoogleTrustServices.EabHmacKey, }) - case sslProviderLetsEncrypt, sslProviderLetsEncryptStaging: reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) - default: err = fmt.Errorf("unsupported ssl provider: %s", sslProviderConfig.Provider) } - if err != nil { return nil, err } @@ -129,7 +127,12 @@ func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderCon return resp.Resource, nil } - if err := repo.Save(sslProviderConfig.Provider, user.GetEmail(), user.getPrivateKeyPEM(), reg); err != nil { + if _, err := repo.Save(context.Background(), &domain.AcmeAccount{ + CA: sslProviderConfig.Provider, + Email: user.GetEmail(), + Key: user.getPrivateKeyPEM(), + Resource: reg, + }); err != nil { return nil, fmt.Errorf("failed to save registration: %w", err) } diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go index a612ebda..5acc8800 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -26,6 +26,7 @@ type ApplyCertResult struct { CertificateFullChain string IssuerCertificate string PrivateKey string + ACMEAccountUrl string ACMECertUrl string ACMECertStableUrl string CSR string @@ -46,8 +47,7 @@ type applicantOptions struct { DnsPropagationTimeout int32 DnsTTL int32 DisableFollowCNAME bool - DisableARI bool - SkipBeforeExpiryDays int32 + 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: 暂时使用代理模式以兼容之前版本代码,后续重新实现此处逻辑 diff --git a/internal/applicant/providers.go b/internal/applicant/providers.go index f53c7287..d992e9c1 100644 --- a/internal/applicant/providers.go +++ b/internal/applicant/providers.go @@ -35,8 +35,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeACMEHttpReq: { access := domain.AccessConfigForACMEHttpReq{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerACMEHttpReq.NewChallengeProvider(&providerACMEHttpReq.ACMEHttpReqApplicantConfig{ @@ -52,8 +52,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeAliyun, domain.ApplyDNSProviderTypeAliyunDNS: { access := domain.AccessConfigForAliyun{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerAliyun.NewChallengeProvider(&providerAliyun.AliyunApplicantConfig{ @@ -68,8 +68,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeAWS, domain.ApplyDNSProviderTypeAWSRoute53: { access := domain.AccessConfigForAWS{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerAWSRoute53.NewChallengeProvider(&providerAWSRoute53.AWSRoute53ApplicantConfig{ @@ -86,8 +86,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeAzureDNS: { access := domain.AccessConfigForAzure{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerAzureDNS.NewChallengeProvider(&providerAzureDNS.AzureDNSApplicantConfig{ @@ -104,8 +104,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeCloudflare: { access := domain.AccessConfigForCloudflare{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerCloudflare.NewChallengeProvider(&providerCloudflare.CloudflareApplicantConfig{ @@ -119,8 +119,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeClouDNS: { access := domain.AccessConfigForClouDNS{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerClouDNS.NewChallengeProvider(&providerClouDNS.ClouDNSApplicantConfig{ @@ -135,8 +135,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeGname: { access := domain.AccessConfigForGname{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerGname.NewChallengeProvider(&providerGname.GnameApplicantConfig{ @@ -151,8 +151,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeGoDaddy: { access := domain.AccessConfigForGoDaddy{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerGoDaddy.NewChallengeProvider(&providerGoDaddy.GoDaddyApplicantConfig{ @@ -167,8 +167,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeHuaweiCloud, domain.ApplyDNSProviderTypeHuaweiCloudDNS: { access := domain.AccessConfigForHuaweiCloud{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerHuaweiCloud.NewChallengeProvider(&providerHuaweiCloud.HuaweiCloudApplicantConfig{ @@ -184,8 +184,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeNameDotCom: { access := domain.AccessConfigForNameDotCom{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerNameDotCom.NewChallengeProvider(&providerNameDotCom.NameDotComApplicantConfig{ @@ -200,8 +200,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeNameSilo: { access := domain.AccessConfigForNameSilo{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerNameSilo.NewChallengeProvider(&providerNameSilo.NameSiloApplicantConfig{ @@ -215,8 +215,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeNS1: { access := domain.AccessConfigForNS1{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerNS1.NewChallengeProvider(&providerNS1.NS1ApplicantConfig{ @@ -230,8 +230,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypePowerDNS: { access := domain.AccessConfigForPowerDNS{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerPowerDNS.NewChallengeProvider(&providerPowerDNS.PowerDNSApplicantConfig{ @@ -246,8 +246,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeRainYun: { access := domain.AccessConfigForRainYun{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerRainYun.NewChallengeProvider(&providerRainYun.RainYunApplicantConfig{ @@ -261,8 +261,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeTencentCloud, domain.ApplyDNSProviderTypeTencentCloudDNS: { access := domain.AccessConfigForTencentCloud{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerTencentCloud.NewChallengeProvider(&providerTencentCloud.TencentCloudApplicantConfig{ @@ -277,8 +277,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeVolcEngine, domain.ApplyDNSProviderTypeVolcEngineDNS: { access := domain.AccessConfigForVolcEngine{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerVolcEngine.NewChallengeProvider(&providerVolcEngine.VolcEngineApplicantConfig{ @@ -293,8 +293,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { case domain.ApplyDNSProviderTypeWestcn: { access := domain.AccessConfigForWestcn{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) } applicant, err := providerWestcn.NewChallengeProvider(&providerWestcn.WestcnApplicantConfig{ diff --git a/internal/certificate/service.go b/internal/certificate/service.go index b8f5fa89..a207d1c3 100644 --- a/internal/certificate/service.go +++ b/internal/certificate/service.go @@ -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 diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 344c78e6..c5f7f40a 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -54,8 +54,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeAliyunALB, domain.DeployProviderTypeAliyunCDN, domain.DeployProviderTypeAliyunCLB, domain.DeployProviderTypeAliyunDCDN, domain.DeployProviderTypeAliyunLive, domain.DeployProviderTypeAliyunNLB, domain.DeployProviderTypeAliyunOSS, domain.DeployProviderTypeAliyunWAF: { access := domain.AccessConfigForAliyun{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } switch options.Provider { @@ -146,8 +146,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeAWSCloudFront: { access := domain.AccessConfigForAWS{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } switch options.Provider { @@ -168,8 +168,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeBaiduCloudCDN: { access := domain.AccessConfigForBaiduCloud{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } switch options.Provider { @@ -189,8 +189,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeBytePlusCDN: { access := domain.AccessConfigForBytePlus{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } switch options.Provider { @@ -210,8 +210,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeDogeCloudCDN: { access := domain.AccessConfigForDogeCloud{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } deployer, err := providerDogeCDN.NewWithLogger(&providerDogeCDN.DogeCloudCDNDeployerConfig{ @@ -225,8 +225,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeEdgioApplications: { access := domain.AccessConfigForEdgio{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } deployer, err := providerEdgioApplications.NewWithLogger(&providerEdgioApplications.EdgioApplicationsDeployerConfig{ @@ -240,8 +240,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeHuaweiCloudCDN, domain.DeployProviderTypeHuaweiCloudELB: { access := domain.AccessConfigForHuaweiCloud{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } switch options.Provider { @@ -291,8 +291,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeKubernetesSecret: { access := domain.AccessConfigForKubernetes{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } deployer, err := providerK8sSecret.NewWithLogger(&providerK8sSecret.K8sSecretDeployerConfig{ @@ -309,8 +309,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeQiniuCDN, domain.DeployProviderTypeQiniuPili: { access := domain.AccessConfigForQiniu{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } switch options.Provider { @@ -339,8 +339,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeSSH: { access := domain.AccessConfigForSSH{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } deployer, err := providerSSH.NewWithLogger(&providerSSH.SshDeployerConfig{ @@ -367,8 +367,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeTencentCloudCDN, domain.DeployProviderTypeTencentCloudCLB, domain.DeployProviderTypeTencentCloudCOS, domain.DeployProviderTypeTencentCloudCSS, domain.DeployProviderTypeTencentCloudECDN, domain.DeployProviderTypeTencentCloudEO: { access := domain.AccessConfigForTencentCloud{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } switch options.Provider { @@ -435,8 +435,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeUCloudUCDN, domain.DeployProviderTypeUCloudUS3: { access := domain.AccessConfigForUCloud{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } switch options.Provider { @@ -468,8 +468,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeVolcEngineCDN, domain.DeployProviderTypeVolcEngineCLB, domain.DeployProviderTypeVolcEngineDCDN, domain.DeployProviderTypeVolcEngineLive, domain.DeployProviderTypeVolcEngineTOS: { access := domain.AccessConfigForVolcEngine{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } switch options.Provider { @@ -525,8 +525,8 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, case domain.DeployProviderTypeWebhook: { access := domain.AccessConfigForWebhook{} - if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { - return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to populate provider access config: %w", err) } deployer, err := providerWebhook.NewWithLogger(&providerWebhook.WebhookDeployerConfig{ diff --git a/internal/domain/certificate.go b/internal/domain/certificate.go index f0e03711..f57a2c4a 100644 --- a/internal/domain/certificate.go +++ b/internal/domain/certificate.go @@ -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") +) diff --git a/internal/domain/dtos/certificate.go b/internal/domain/dtos/certificate.go index cf9eb785..a1853df0 100644 --- a/internal/domain/dtos/certificate.go +++ b/internal/domain/dtos/certificate.go @@ -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"` } diff --git a/internal/domain/dtos/workflow.go b/internal/domain/dtos/workflow.go index 9d1f5781..3988220b 100644 --- a/internal/domain/dtos/workflow.go +++ b/internal/domain/dtos/workflow.go @@ -4,7 +4,7 @@ import "github.com/usual2970/certimate/internal/domain" type WorkflowStartRunReq struct { WorkflowId string `json:"-"` - Trigger domain.WorkflowTriggerType `json:"trigger"` + RunTrigger domain.WorkflowTriggerType `json:"trigger"` } type WorkflowCancelRunReq struct { diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index ac8fbce6..2766018d 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -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 提供商额外配置 diff --git a/internal/domain/workflow_output.go b/internal/domain/workflow_output.go index 44a70bc2..57af169b 100644 --- a/internal/domain/workflow_output.go +++ b/internal/domain/workflow_output.go @@ -5,6 +5,7 @@ const CollectionNameWorkflowOutput = "workflow_output" type WorkflowOutput struct { Meta WorkflowId string `json:"workflowId" db:"workflow"` + RunId string `json:"runId" db:"runId"` NodeId string `json:"nodeId" db:"nodeId"` Node *WorkflowNode `json:"node" db:"node"` Outputs []WorkflowNodeIO `json:"outputs" db:"outputs"` diff --git a/internal/domain/workflow_run.go b/internal/domain/workflow_run.go index 25ba1d7a..ff5424c5 100644 --- a/internal/domain/workflow_run.go +++ b/internal/domain/workflow_run.go @@ -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 { diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/gname/internal/lego.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/gname/internal/lego.go index 979a803b..03dc633f 100644 --- a/internal/pkg/core/applicant/acme-dns-01/lego-providers/gname/internal/lego.go +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/gname/internal/lego.go @@ -173,8 +173,6 @@ func (d *DNSProvider) addOrUpdateDNSRecord(domain, subDomain, value string) erro _, err := d.client.ModifyDomainResolution(request) return err } - - return nil } func (d *DNSProvider) removeDNSRecord(domain, subDomain, value string) error { diff --git a/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go b/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go index d4d70e04..5a6d4bf2 100644 --- a/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go +++ b/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go @@ -2,13 +2,13 @@ import ( "context" - "encoding/pem" "errors" xerrors "github.com/pkg/errors" "github.com/usual2970/certimate/internal/pkg/core/deployer" "github.com/usual2970/certimate/internal/pkg/core/logger" + "github.com/usual2970/certimate/internal/pkg/utils/certs" edgsdk "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7" edgsdkDtos "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7/dtos" ) @@ -57,7 +57,10 @@ func NewWithLogger(config *EdgioApplicationsDeployerConfig, logger logger.Logger func (d *EdgioApplicationsDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) { // 提取 Edgio 所需的服务端证书和中间证书内容 - privateCertPem, intermediateCertPem := extractCertChains(certPem) + privateCertPem, intermediateCertPem, err := certs.ExtractCertificatesFromPEM(certPem) + if err != nil { + return nil, err + } // 上传 TLS 证书 // REF: https://docs.edg.io/rest_api/#tag/tls-certs/operation/postConfigV01TlsCerts @@ -81,32 +84,3 @@ func createSdkClient(clientId, clientSecret string) (*edgsdk.EdgioClient, error) client := edgsdk.NewEdgioClient(clientId, clientSecret, "", "") return client, nil } - -func extractCertChains(certPem string) (primaryCertPem string, intermediateCertPem string) { - pemBlocks := make([]*pem.Block, 0) - pemData := []byte(certPem) - for { - block, rest := pem.Decode(pemData) - if block == nil { - break - } - - pemBlocks = append(pemBlocks, block) - pemData = rest - } - - primaryCertPem = "" - intermediateCertPem = "" - - if len(pemBlocks) > 0 { - primaryCertPem = string(pem.EncodeToMemory(pemBlocks[0])) - } - - if len(pemBlocks) > 1 { - for i := 1; i < len(pemBlocks); i++ { - intermediateCertPem += string(pem.EncodeToMemory(pemBlocks[i])) - } - } - - return primaryCertPem, intermediateCertPem -} diff --git a/internal/pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn.go b/internal/pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn.go index 6591da1b..8fac6459 100644 --- a/internal/pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn.go +++ b/internal/pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn.go @@ -78,7 +78,7 @@ func (d *QiniuCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPe // 获取域名信息 // REF: https://developer.qiniu.com/fusion/4246/the-domain-name - getDomainInfoResp, err := d.sdkClient.GetDomainInfo(domain) + getDomainInfoResp, err := d.sdkClient.GetDomainInfo(context.TODO(), domain) if err != nil { return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetDomainInfo'") } @@ -88,14 +88,14 @@ func (d *QiniuCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPe // 判断域名是否已启用 HTTPS。如果已启用,修改域名证书;否则,启用 HTTPS // REF: https://developer.qiniu.com/fusion/4246/the-domain-name if getDomainInfoResp.Https != nil && getDomainInfoResp.Https.CertID != "" { - modifyDomainHttpsConfResp, err := d.sdkClient.ModifyDomainHttpsConf(domain, upres.CertId, getDomainInfoResp.Https.ForceHttps, getDomainInfoResp.Https.Http2Enable) + modifyDomainHttpsConfResp, err := d.sdkClient.ModifyDomainHttpsConf(context.TODO(), domain, upres.CertId, getDomainInfoResp.Https.ForceHttps, getDomainInfoResp.Https.Http2Enable) if err != nil { return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.ModifyDomainHttpsConf'") } d.logger.Logt("已修改域名证书", modifyDomainHttpsConfResp) } else { - enableDomainHttpsResp, err := d.sdkClient.EnableDomainHttps(domain, upres.CertId, true, true) + enableDomainHttpsResp, err := d.sdkClient.EnableDomainHttps(context.TODO(), domain, upres.CertId, true, true) if err != nil { return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.EnableDomainHttps'") } diff --git a/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go b/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go index a599cbe2..851cbf01 100644 --- a/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go +++ b/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go @@ -60,7 +60,7 @@ func (u *QiniuSSLCertUploader) Upload(ctx context.Context, certPem string, privk // 上传新证书 // REF: https://developer.qiniu.com/fusion/8593/interface-related-certificate - uploadSslCertResp, err := u.sdkClient.UploadSslCert(certName, certX509.Subject.CommonName, certPem, privkeyPem) + uploadSslCertResp, err := u.sdkClient.UploadSslCert(context.TODO(), certName, certX509.Subject.CommonName, certPem, privkeyPem) if err != nil { return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.UploadSslCert'") } diff --git a/internal/pkg/utils/certs/extractor.go b/internal/pkg/utils/certs/extractor.go new file mode 100644 index 00000000..bf07b4f3 --- /dev/null +++ b/internal/pkg/utils/certs/extractor.go @@ -0,0 +1,48 @@ +package certs + +import ( + "encoding/pem" + "errors" +) + +// 从 PEM 编码的证书字符串解析并提取服务器证书和中间证书。 +// +// 入参: +// - certPem: 证书 PEM 内容。 +// +// 出参: +// - serverCertPem: 服务器证书的 PEM 内容。 +// - interCertPem: 中间证书的 PEM 内容。 +// - err: 错误。 +func ExtractCertificatesFromPEM(certPem string) (serverCertPem string, interCertPem string, err error) { + pemBlocks := make([]*pem.Block, 0) + pemData := []byte(certPem) + for { + block, rest := pem.Decode(pemData) + if block == nil || block.Type != "CERTIFICATE" { + break + } + + pemBlocks = append(pemBlocks, block) + pemData = rest + } + + serverCertPem = "" + interCertPem = "" + + if len(pemBlocks) == 0 { + return "", "", errors.New("failed to decode PEM block") + } + + if len(pemBlocks) > 0 { + serverCertPem = string(pem.EncodeToMemory(pemBlocks[0])) + } + + if len(pemBlocks) > 1 { + for i := 1; i < len(pemBlocks); i++ { + interCertPem += string(pem.EncodeToMemory(pemBlocks[i])) + } + } + + return serverCertPem, interCertPem, nil +} diff --git a/internal/pkg/utils/certs/parser.go b/internal/pkg/utils/certs/parser.go index 89338336..af62b03a 100644 --- a/internal/pkg/utils/certs/parser.go +++ b/internal/pkg/utils/certs/parser.go @@ -13,6 +13,7 @@ import ( ) // 从 PEM 编码的证书字符串解析并返回一个 x509.Certificate 对象。 +// PEM 内容可能是包含多张证书的证书链,但只返回第一个证书(即服务器证书)。 // // 入参: // - certPem: 证书 PEM 内容。 diff --git a/internal/pkg/utils/maps/maps.go b/internal/pkg/utils/maps/maps.go index a33d34aa..a88b6629 100644 --- a/internal/pkg/utils/maps/maps.go +++ b/internal/pkg/utils/maps/maps.go @@ -183,7 +183,7 @@ func GetValueOrDefaultAsBool(dict map[string]any, key string, defaultValue bool) return defaultValue } -// 将字典解码为指定类型的结构体。 +// 将字典填充到指定类型的结构体。 // 与 [json.Unmarshal] 类似,但传入的是一个 [map[string]interface{}] 对象而非 JSON 格式的字符串。 // // 入参: @@ -191,8 +191,8 @@ func GetValueOrDefaultAsBool(dict map[string]any, key string, defaultValue bool) // - output: 结构体指针。 // // 出参: -// - 错误信息。如果解码失败,则返回错误信息。 -func Decode(dict map[string]any, output any) error { +// - 错误信息。如果填充失败,则返回错误信息。 +func Populate(dict map[string]any, output any) error { config := &mapstructure.DecoderConfig{ Metadata: nil, Result: output, @@ -207,3 +207,8 @@ func Decode(dict map[string]any, output any) error { return decoder.Decode(dict) } + +// Deprecated: Use [Populate] instead. +func Decode(dict map[string]any, output any) error { + return Populate(dict, output) +} diff --git a/internal/pkg/vendors/gname-sdk/client.go b/internal/pkg/vendors/gname-sdk/client.go index d034cfeb..507555e9 100644 --- a/internal/pkg/vendors/gname-sdk/client.go +++ b/internal/pkg/vendors/gname-sdk/client.go @@ -150,7 +150,7 @@ func (c *GnameClient) sendRequestWithResult(path string, params map[string]any, if err := json.Unmarshal(resp.Body(), &jsonResp); err != nil { return fmt.Errorf("failed to parse response: %w", err) } - if err := maps.Decode(jsonResp, &result); err != nil { + if err := maps.Populate(jsonResp, &result); err != nil { return fmt.Errorf("failed to parse response: %w", err) } diff --git a/internal/pkg/vendors/qiniu-sdk/auth.go b/internal/pkg/vendors/qiniu-sdk/auth.go new file mode 100644 index 00000000..d27fbe03 --- /dev/null +++ b/internal/pkg/vendors/qiniu-sdk/auth.go @@ -0,0 +1,29 @@ +package qiniusdk + +import ( + "net/http" + + "github.com/qiniu/go-sdk/v7/auth" +) + +type transport struct { + http.RoundTripper + mac *auth.Credentials +} + +func newTransport(mac *auth.Credentials, tr http.RoundTripper) *transport { + if tr == nil { + tr = http.DefaultTransport + } + return &transport{tr, mac} +} + +func (t *transport) RoundTrip(req *http.Request) (resp *http.Response, err error) { + token, err := t.mac.SignRequestV2(req) + if err != nil { + return + } + + req.Header.Set("Authorization", "Qiniu "+token) + return t.RoundTripper.RoundTrip(req) +} diff --git a/internal/pkg/vendors/qiniu-sdk/client.go b/internal/pkg/vendors/qiniu-sdk/client.go index 1f036afd..8f9d10e9 100644 --- a/internal/pkg/vendors/qiniu-sdk/client.go +++ b/internal/pkg/vendors/qiniu-sdk/client.go @@ -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 } diff --git a/internal/pkg/vendors/qiniu-sdk/models.go b/internal/pkg/vendors/qiniu-sdk/models.go index 3c68ee6b..dceca028 100644 --- a/internal/pkg/vendors/qiniu-sdk/models.go +++ b/internal/pkg/vendors/qiniu-sdk/models.go @@ -13,7 +13,7 @@ type UploadSslCertRequest struct { } type UploadSslCertResponse struct { - *BaseResponse + BaseResponse CertID string `json:"certID"` } diff --git a/internal/repository/acme_account.go b/internal/repository/acme_account.go index ef8ed62f..020f1aeb 100644 --- a/internal/repository/acme_account.go +++ b/internal/repository/acme_account.go @@ -1,6 +1,9 @@ package repository import ( + "context" + "database/sql" + "errors" "fmt" "github.com/go-acme/lego/v4/registration" @@ -48,18 +51,37 @@ func (r *AcmeAccountRepository) GetByCAAndEmail(ca, email string) (*domain.AcmeA return r.castRecordToModel(record) } -func (r *AcmeAccountRepository) Save(ca, email, key string, resource *registration.Resource) error { +func (r *AcmeAccountRepository) Save(ctx context.Context, acmeAccount *domain.AcmeAccount) (*domain.AcmeAccount, error) { collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameAcmeAccount) if err != nil { - return err + return acmeAccount, err } - record := core.NewRecord(collection) - record.Set("ca", ca) - record.Set("email", email) - record.Set("key", key) - record.Set("resource", resource) - return app.GetApp().Save(record) + var record *core.Record + if acmeAccount.Id == "" { + record = core.NewRecord(collection) + } else { + record, err = app.GetApp().FindRecordById(collection, acmeAccount.Id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return acmeAccount, domain.ErrRecordNotFound + } + return acmeAccount, err + } + } + + record.Set("ca", acmeAccount.CA) + record.Set("email", acmeAccount.Email) + record.Set("key", acmeAccount.Key) + record.Set("resource", acmeAccount.Resource) + if err := app.GetApp().Save(record); err != nil { + return acmeAccount, err + } + + acmeAccount.Id = record.Id + acmeAccount.CreatedAt = record.GetDateTime("created").Time() + acmeAccount.UpdatedAt = record.GetDateTime("updated").Time() + return acmeAccount, nil } func (r *AcmeAccountRepository) castRecordToModel(record *core.Record) (*domain.AcmeAccount, error) { diff --git a/internal/repository/certificate.go b/internal/repository/certificate.go index 5e1a7f8d..0695ca47 100644 --- a/internal/repository/certificate.go +++ b/internal/repository/certificate.go @@ -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"), } diff --git a/internal/repository/workflow.go b/internal/repository/workflow.go index edf7cf7f..baa5e21b 100644 --- a/internal/repository/workflow.go +++ b/internal/repository/workflow.go @@ -24,7 +24,7 @@ func (r *WorkflowRepository) ListEnabledAuto(ctx context.Context) ([]*domain.Wor "enabled={:enabled} && trigger={:trigger}", "-created", 0, 0, - dbx.Params{"enabled": true, "trigger": domain.WorkflowTriggerTypeAuto}, + dbx.Params{"enabled": true, "trigger": string(domain.WorkflowTriggerTypeAuto)}, ) if err != nil { return nil, err @@ -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") diff --git a/internal/repository/workflow_output.go b/internal/repository/workflow_output.go index 724bb799..4cee625c 100644 --- a/internal/repository/workflow_output.go +++ b/internal/repository/workflow_output.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "fmt" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" @@ -17,13 +18,13 @@ func NewWorkflowOutputRepository() *WorkflowOutputRepository { return &WorkflowOutputRepository{} } -func (r *WorkflowOutputRepository) GetByNodeId(ctx context.Context, nodeId string) (*domain.WorkflowOutput, error) { +func (r *WorkflowOutputRepository) GetByNodeId(ctx context.Context, workflowNodeId string) (*domain.WorkflowOutput, error) { records, err := app.GetApp().FindRecordsByFilter( domain.CollectionNameWorkflowOutput, "nodeId={:nodeId}", "-created", 1, 0, - dbx.Params{"nodeId": nodeId}, + dbx.Params{"nodeId": workflowNodeId}, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -34,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 } diff --git a/internal/repository/workflow_run.go b/internal/repository/workflow_run.go new file mode 100644 index 00000000..b1a5234b --- /dev/null +++ b/internal/repository/workflow_run.go @@ -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 +} diff --git a/internal/rest/handlers/certificate.go b/internal/rest/handlers/certificate.go index ded0db94..01b2b06d 100644 --- a/internal/rest/handlers/certificate.go +++ b/internal/rest/handlers/certificate.go @@ -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) } } diff --git a/internal/rest/handlers/workflow.go b/internal/rest/handlers/workflow.go index 83b3302b..bad474f0 100644 --- a/internal/rest/handlers/workflow.go +++ b/internal/rest/handlers/workflow.go @@ -13,7 +13,7 @@ import ( type workflowService interface { StartRun(ctx context.Context, req *dtos.WorkflowStartRunReq) error CancelRun(ctx context.Context, req *dtos.WorkflowCancelRunReq) error - Stop(ctx context.Context) + Shutdown(ctx context.Context) } type WorkflowHandler struct { diff --git a/internal/rest/routes/routes.go b/internal/rest/routes/routes.go index 58b4f0f1..87fcb297 100644 --- a/internal/rest/routes/routes.go +++ b/internal/rest/routes/routes.go @@ -27,13 +27,14 @@ func Register(router *router.Router[*core.RequestEvent]) { certificateSvc = certificate.NewCertificateService(certificateRepo) workflowRepo := repository.NewWorkflowRepository() - workflowSvc = workflow.NewWorkflowService(workflowRepo) + workflowRunRepo := repository.NewWorkflowRunRepository() + workflowSvc = workflow.NewWorkflowService(workflowRepo, workflowRunRepo) statisticsRepo := repository.NewStatisticsRepository() statisticsSvc = statistics.NewStatisticsService(statisticsRepo) - notifyRepo := repository.NewSettingsRepository() - notifySvc = notify.NewNotifyService(notifyRepo) + settingsRepo := repository.NewSettingsRepository() + notifySvc = notify.NewNotifyService(settingsRepo) group := router.Group("/api") group.Bind(apis.RequireSuperuserAuth()) @@ -45,6 +46,6 @@ func Register(router *router.Router[*core.RequestEvent]) { func Unregister() { if workflowSvc != nil { - workflowSvc.Stop(context.Background()) + workflowSvc.Shutdown(context.Background()) } } diff --git a/internal/scheduler/certificate.go b/internal/scheduler/certificate.go index 26c7311f..43887cb0 100644 --- a/internal/scheduler/certificate.go +++ b/internal/scheduler/certificate.go @@ -6,6 +6,6 @@ type certificateService interface { InitSchedule(ctx context.Context) error } -func NewCertificateScheduler(service certificateService) error { +func InitCertificateScheduler(service certificateService) error { return service.InitSchedule(context.Background()) } diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go index 055beddb..91dbf115 100644 --- a/internal/scheduler/scheduler.go +++ b/internal/scheduler/scheduler.go @@ -1,6 +1,7 @@ package scheduler import ( + "github.com/usual2970/certimate/internal/app" "github.com/usual2970/certimate/internal/certificate" "github.com/usual2970/certimate/internal/repository" "github.com/usual2970/certimate/internal/workflow" @@ -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) + } } diff --git a/internal/scheduler/workflow.go b/internal/scheduler/workflow.go index 7cb4dfa8..cef4adce 100644 --- a/internal/scheduler/workflow.go +++ b/internal/scheduler/workflow.go @@ -6,6 +6,6 @@ type workflowService interface { InitSchedule(ctx context.Context) error } -func NewWorkflowScheduler(service workflowService) error { +func InitWorkflowScheduler(service workflowService) error { return service.InitSchedule(context.Background()) } diff --git a/internal/statistics/service.go b/internal/statistics/service.go index 3b1f5876..44388ba9 100644 --- a/internal/statistics/service.go +++ b/internal/statistics/service.go @@ -11,15 +11,15 @@ type statisticsRepository interface { } type StatisticsService struct { - repo statisticsRepository + statRepo statisticsRepository } -func NewStatisticsService(repo statisticsRepository) *StatisticsService { +func NewStatisticsService(statRepo statisticsRepository) *StatisticsService { return &StatisticsService{ - repo: repo, + statRepo: statRepo, } } func (s *StatisticsService) Get(ctx context.Context) (*domain.Statistics, error) { - return s.repo.Get(ctx) + return s.statRepo.Get(ctx) } diff --git a/internal/workflow/dispatcher/dispatcher.go b/internal/workflow/dispatcher/dispatcher.go new file mode 100644 index 00000000..53081b6b --- /dev/null +++ b/internal/workflow/dispatcher/dispatcher.go @@ -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) + } + } +} diff --git a/internal/workflow/dispatcher/invoker.go b/internal/workflow/dispatcher/invoker.go new file mode 100644 index 00000000..d35cca3e --- /dev/null +++ b/internal/workflow/dispatcher/invoker.go @@ -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 +} diff --git a/internal/workflow/dispatcher/singleton.go b/internal/workflow/dispatcher/singleton.go new file mode 100644 index 00000000..b5834c48 --- /dev/null +++ b/internal/workflow/dispatcher/singleton.go @@ -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 +} diff --git a/internal/workflow/event.go b/internal/workflow/event.go index f8117dbc..0fedd67b 100644 --- a/internal/workflow/event.go +++ b/internal/workflow/event.go @@ -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) } diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index 5ca379c4..3c974b8c 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -3,7 +3,6 @@ package nodeprocessor import ( "context" "fmt" - "strings" "time" "golang.org/x/exp/maps" @@ -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) + } } } diff --git a/internal/workflow/node-processor/condition_node.go b/internal/workflow/node-processor/condition_node.go index cd3ab07f..499a5004 100644 --- a/internal/workflow/node-processor/condition_node.go +++ b/internal/workflow/node-processor/condition_node.go @@ -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 } diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go index 48e344ad..c7ab964f 100644 --- a/internal/workflow/node-processor/deploy_node.go +++ b/internal/workflow/node-processor/deploy_node.go @@ -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, "配置项变化:主机提供商授权" diff --git a/internal/workflow/node-processor/execute_failure_node.go b/internal/workflow/node-processor/execute_failure_node.go index d1ff0034..2516edb4 100644 --- a/internal/workflow/node-processor/execute_failure_node.go +++ b/internal/workflow/node-processor/execute_failure_node.go @@ -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 } diff --git a/internal/workflow/node-processor/execute_success_node.go b/internal/workflow/node-processor/execute_success_node.go index d8d4139f..a7833a53 100644 --- a/internal/workflow/node-processor/execute_success_node.go +++ b/internal/workflow/node-processor/execute_success_node.go @@ -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 } diff --git a/internal/workflow/node-processor/notify_node.go b/internal/workflow/node-processor/notify_node.go index 0ba5eb1f..e4c3da2d 100644 --- a/internal/workflow/node-processor/notify_node.go +++ b/internal/workflow/node-processor/notify_node.go @@ -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 ¬ifyNode{ - 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 } diff --git a/internal/workflow/node-processor/processor.go b/internal/workflow/node-processor/processor.go index 55e8477a..08712280 100644 --- a/internal/workflow/node-processor/processor.go +++ b/internal/workflow/node-processor/processor.go @@ -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) +} diff --git a/internal/workflow/node-processor/start_node.go b/internal/workflow/node-processor/start_node.go index 81d93de6..7d04685a 100644 --- a/internal/workflow/node-processor/start_node.go +++ b/internal/workflow/node-processor/start_node.go @@ -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 } diff --git a/internal/workflow/node-processor/upload_node.go b/internal/workflow/node-processor/upload_node.go index 8aa0bba7..99108481 100644 --- a/internal/workflow/node-processor/upload_node.go +++ b/internal/workflow/node-processor/upload_node.go @@ -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, "" +} diff --git a/internal/workflow/processor/processor.go b/internal/workflow/processor/processor.go deleted file mode 100644 index 47663136..00000000 --- a/internal/workflow/processor/processor.go +++ /dev/null @@ -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 -} diff --git a/internal/workflow/service.go b/internal/workflow/service.go index 0a5f2f96..d2236a8d 100644 --- a/internal/workflow/service.go +++ b/internal/workflow/service.go @@ -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() +} diff --git a/main.go b/main.go index 1928c46b..2c3d84c2 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,8 @@ import ( "github.com/usual2970/certimate/internal/scheduler" "github.com/usual2970/certimate/internal/workflow" "github.com/usual2970/certimate/ui" - //_ "github.com/usual2970/certimate/migrations" + + _ "github.com/usual2970/certimate/migrations" ) func main() { diff --git a/migrations/1737141502_superusers_initial.go b/migrations/1737141502_superusers_initial.go index a9dd9522..4440ed76 100644 --- a/migrations/1737141502_superusers_initial.go +++ b/migrations/1737141502_superusers_initial.go @@ -12,16 +12,16 @@ func init() { return err } - record := core.NewRecord(superusers) - record.Set("email", "admin@certimate.fun") - record.Set("password", "1234567890") - return app.Save(record) - }, func(app core.App) error { record, _ := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "admin@certimate.fun") if record == nil { - return nil + record := core.NewRecord(superusers) + record.Set("email", "admin@certimate.fun") + record.Set("password", "1234567890") + return app.Save(record) } - return app.Delete(record) + return nil + }, func(app core.App) error { + return nil }) } diff --git a/migrations/1738767422_updated_certificate.go b/migrations/1738767422_updated_certificate.go new file mode 100644 index 00000000..e5dfe573 --- /dev/null +++ b/migrations/1738767422_updated_certificate.go @@ -0,0 +1,127 @@ +package migrations + +import ( + "encoding/json" + + "github.com/pocketbase/pocketbase/core" + m "github.com/pocketbase/pocketbase/migrations" +) + +func init() { + m.Register(func(app core.App) error { + collection, err := app.FindCollectionByNameOrId("4szxr9x43tpj6np") + if err != nil { + return err + } + + // update collection data + if err := json.Unmarshal([]byte(`{ + "indexes": [ + "CREATE INDEX ` + "`" + `idx_Jx8TXzDCmw` + "`" + ` ON ` + "`" + `certificate` + "`" + ` (` + "`" + `workflowId` + "`" + `)", + "CREATE INDEX ` + "`" + `idx_kcKpgAZapk` + "`" + ` ON ` + "`" + `certificate` + "`" + ` (` + "`" + `workflowNodeId` + "`" + `)" + ] + }`), &collection); err != nil { + return err + } + + // add field + if err := collection.Fields.AddMarshaledJSONAt(3, []byte(`{ + "autogeneratePattern": "", + "hidden": false, + "id": "text2069360702", + "max": 0, + "min": 0, + "name": "serialNumber", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }`)); err != nil { + return err + } + + // add field + if err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{ + "autogeneratePattern": "", + "hidden": false, + "id": "text2910474005", + "max": 0, + "min": 0, + "name": "issuer", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }`)); err != nil { + return err + } + + // add field + if err := collection.Fields.AddMarshaledJSONAt(8, []byte(`{ + "autogeneratePattern": "", + "hidden": false, + "id": "text4164403445", + "max": 0, + "min": 0, + "name": "keyAlgorithm", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }`)); err != nil { + return err + } + + // add field + if err := collection.Fields.AddMarshaledJSONAt(11, []byte(`{ + "autogeneratePattern": "", + "hidden": false, + "id": "text2045248758", + "max": 0, + "min": 0, + "name": "acmeAccountUrl", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }`)); err != nil { + return err + } + + return app.Save(collection) + }, func(app core.App) error { + collection, err := app.FindCollectionByNameOrId("4szxr9x43tpj6np") + if err != nil { + return err + } + + // update collection data + if err := json.Unmarshal([]byte(`{ + "indexes": [] + }`), &collection); err != nil { + return err + } + + // remove field + collection.Fields.RemoveById("text2069360702") + + // remove field + collection.Fields.RemoveById("text2910474005") + + // remove field + collection.Fields.RemoveById("text4164403445") + + // remove field + collection.Fields.RemoveById("text2045248758") + + return app.Save(collection) + }) +} diff --git a/migrations/1737479489_updated_workflow.go b/migrations/1738828775_updated_workflow.go similarity index 100% rename from migrations/1737479489_updated_workflow.go rename to migrations/1738828775_updated_workflow.go diff --git a/migrations/1737479538_updated_workflow_run.go b/migrations/1738828788_updated_workflow_run.go similarity index 100% rename from migrations/1737479538_updated_workflow_run.go rename to migrations/1738828788_updated_workflow_run.go diff --git a/migrations/1738839725_updated_certificate.go b/migrations/1738839725_updated_certificate.go new file mode 100644 index 00000000..447d5297 --- /dev/null +++ b/migrations/1738839725_updated_certificate.go @@ -0,0 +1,67 @@ +package migrations + +import ( + "encoding/json" + + "github.com/pocketbase/pocketbase/core" + m "github.com/pocketbase/pocketbase/migrations" +) + +func init() { + m.Register(func(app core.App) error { + collection, err := app.FindCollectionByNameOrId("4szxr9x43tpj6np") + if err != nil { + return err + } + + // update collection data + if err := json.Unmarshal([]byte(`{ + "indexes": [ + "CREATE INDEX ` + "`" + `idx_Jx8TXzDCmw` + "`" + ` ON ` + "`" + `certificate` + "`" + ` (` + "`" + `workflowId` + "`" + `)", + "CREATE INDEX ` + "`" + `idx_kcKpgAZapk` + "`" + ` ON ` + "`" + `certificate` + "`" + ` (` + "`" + `workflowNodeId` + "`" + `)", + "CREATE INDEX ` + "`" + `idx_2cRXqNDyyp` + "`" + ` ON ` + "`" + `certificate` + "`" + ` (` + "`" + `workflowRunId` + "`" + `)" + ] + }`), &collection); err != nil { + return err + } + + // add field + if err := collection.Fields.AddMarshaledJSONAt(15, []byte(`{ + "cascadeDelete": false, + "collectionId": "qjp8lygssgwyqyz", + "hidden": false, + "id": "relation3917999135", + "maxSelect": 1, + "minSelect": 0, + "name": "workflowRunId", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }`)); err != nil { + return err + } + + return app.Save(collection) + }, func(app core.App) error { + collection, err := app.FindCollectionByNameOrId("4szxr9x43tpj6np") + if err != nil { + return err + } + + // update collection data + if err := json.Unmarshal([]byte(`{ + "indexes": [ + "CREATE INDEX ` + "`" + `idx_Jx8TXzDCmw` + "`" + ` ON ` + "`" + `certificate` + "`" + ` (` + "`" + `workflowId` + "`" + `)", + "CREATE INDEX ` + "`" + `idx_kcKpgAZapk` + "`" + ` ON ` + "`" + `certificate` + "`" + ` (` + "`" + `workflowNodeId` + "`" + `)" + ] + }`), &collection); err != nil { + return err + } + + // remove field + collection.Fields.RemoveById("relation3917999135") + + return app.Save(collection) + }) +} diff --git a/migrations/1738840633_updated_workflow_output.go b/migrations/1738840633_updated_workflow_output.go new file mode 100644 index 00000000..6e836a76 --- /dev/null +++ b/migrations/1738840633_updated_workflow_output.go @@ -0,0 +1,63 @@ +package migrations + +import ( + "encoding/json" + + "github.com/pocketbase/pocketbase/core" + m "github.com/pocketbase/pocketbase/migrations" +) + +func init() { + m.Register(func(app core.App) error { + collection, err := app.FindCollectionByNameOrId("bqnxb95f2cooowp") + if err != nil { + return err + } + + // update collection data + if err := json.Unmarshal([]byte(`{ + "indexes": [ + "CREATE INDEX ` + "`" + `idx_BYoQPsz4my` + "`" + ` ON ` + "`" + `workflow_output` + "`" + ` (` + "`" + `workflowId` + "`" + `)", + "CREATE INDEX ` + "`" + `idx_O9zxLETuxJ` + "`" + ` ON ` + "`" + `workflow_output` + "`" + ` (` + "`" + `runId` + "`" + `)" + ] + }`), &collection); err != nil { + return err + } + + // add field + if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{ + "cascadeDelete": false, + "collectionId": "qjp8lygssgwyqyz", + "hidden": false, + "id": "relation821863227", + "maxSelect": 1, + "minSelect": 0, + "name": "runId", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }`)); err != nil { + return err + } + + return app.Save(collection) + }, func(app core.App) error { + collection, err := app.FindCollectionByNameOrId("bqnxb95f2cooowp") + if err != nil { + return err + } + + // update collection data + if err := json.Unmarshal([]byte(`{ + "indexes": [] + }`), &collection); err != nil { + return err + } + + // remove field + collection.Fields.RemoveById("relation821863227") + + return app.Save(collection) + }) +} diff --git a/migrations/1739263253_updated_workflow_run.go b/migrations/1739263253_updated_workflow_run.go new file mode 100644 index 00000000..5286a7b8 --- /dev/null +++ b/migrations/1739263253_updated_workflow_run.go @@ -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) + }) +} diff --git a/migrations/1739263264_updated_workflow_output.go b/migrations/1739263264_updated_workflow_output.go new file mode 100644 index 00000000..e2add066 --- /dev/null +++ b/migrations/1739263264_updated_workflow_output.go @@ -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) + }) +} diff --git a/ui/src/api/certificates.ts b/ui/src/api/certificates.ts index 3c00fdcf..6a8935bb 100644 --- a/ui/src/api/certificates.ts +++ b/ui/src/api/certificates.ts @@ -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>(`/api/certificates/${encodeURIComponent(certificateId)}/archive`, { + const resp = await pb.send>(`/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(`/api/certificates/validate/private-key`, { + const resp = await pb.send>(`/api/certificates/validate/private-key`, { method: "POST", headers: { "Content-Type": "application/json", diff --git a/ui/src/components/certificate/CertificateDetail.tsx b/ui/src/components/certificate/CertificateDetail.tsx index 6feb992b..6c842a36 100644 --- a/ui/src/components/certificate/CertificateDetail.tsx +++ b/ui/src/components/certificate/CertificateDetail.tsx @@ -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) => {
- + + + + + - + + + + + + + + + @@ -59,7 +75,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => { - + @@ -76,7 +92,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => { - +
diff --git a/ui/src/components/workflow/WorkflowRunDetail.tsx b/ui/src/components/workflow/WorkflowRunDetail.tsx new file mode 100644 index 00000000..22ad5b1f --- /dev/null +++ b/ui/src/components/workflow/WorkflowRunDetail.tsx @@ -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 ( +
+ + {t("workflow_run.props.status.succeeded")}} /> + + + + {t("workflow_run.props.status.failed")}} /> + + +
+ {t("workflow_run.logs")} +
+
+ {data.logs?.map((item, i) => { + return ( +
+
{item.nodeName}
+
+ {item.records?.map((output, j) => { + return ( +
+
[{dayjs(output.time).format("YYYY-MM-DD HH:mm:ss")}]
+ {output.error ?
{output.error}
:
{output.content}
} +
+ ); + })} +
+
+ ); + })} +
+
+
+ + + + + + +
+ ); +}; + +const WorkflowRunArtifacts = ({ runId }: { runId: string }) => { + const { t } = useTranslation(); + + const [notificationApi, NotificationContextHolder] = notification.useNotification(); + + const tableColumns: TableProps["columns"] = [ + { + key: "$index", + align: "center", + fixed: "left", + width: 50, + render: (_, __, index) => index + 1, + }, + { + key: "type", + title: t("workflow_run_artifact.props.type"), + render: () => t("workflow_run_artifact.props.type.certificate"), + }, + { + key: "name", + title: t("workflow_run_artifact.props.name"), + ellipsis: true, + render: (_, record) => { + return ( + + {record.subjectAltNames} + + ); + }, + }, + { + key: "$action", + align: "end", + width: 120, + render: (_, record) => ( + + + diff --git a/ui/src/pages/workflows/WorkflowList.tsx b/ui/src/pages/workflows/WorkflowList.tsx index 28b776e7..edadd9c0 100644 --- a/ui/src/pages/workflows/WorkflowList.tsx +++ b/ui/src/pages/workflows/WorkflowList.tsx @@ -7,8 +7,8 @@ import { CloseCircleOutlined as CloseCircleOutlinedIcon, DeleteOutlined as DeleteOutlinedIcon, EditOutlined as EditOutlinedIcon, - PauseCircleOutlined as PauseCircleOutlinedIcon, PlusOutlined as PlusOutlinedIcon, + StopOutlined as StopOutlinedIcon, SyncOutlined as SyncOutlinedIcon, } from "@ant-design/icons"; @@ -170,7 +170,7 @@ const WorkflowList = () => { } else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.FAILED) { icon = ; } else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.CANCELED) { - icon = ; + icon = ; } return ( @@ -350,7 +350,7 @@ const WorkflowList = () => { dataSource={tableData} loading={loading} locale={{ - emptyText: , + emptyText: , }} 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)" }} /> diff --git a/ui/src/repository/_pocketbase.ts b/ui/src/repository/_pocketbase.ts index 53d2f792..983c4987 100644 --- a/ui/src/repository/_pocketbase.ts +++ b/ui/src/repository/_pocketbase.ts @@ -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"; diff --git a/ui/src/repository/access.ts b/ui/src/repository/access.ts index c254f386..4b21d5da 100644 --- a/ui/src/repository/access.ts +++ b/ui/src/repository/access.ts @@ -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({ + return await getPocketBase().collection(COLLECTION_NAME_ACCESS).getFullList({ filter: "deleted=null", sort: "-created", requestKey: null, @@ -15,15 +13,15 @@ export const list = async () => { export const save = async (record: MaybeModelRecord) => { if (record.id) { - return await getPocketBase().collection(COLLECTION_NAME).update(record.id, record); + return await getPocketBase().collection(COLLECTION_NAME_ACCESS).update(record.id, record); } - return await getPocketBase().collection(COLLECTION_NAME).create(record); + return await getPocketBase().collection(COLLECTION_NAME_ACCESS).create(record); }; export const remove = async (record: MaybeModelRecordWithId) => { await getPocketBase() - .collection(COLLECTION_NAME) + .collection(COLLECTION_NAME_ACCESS) .update(record.id!, { deleted: dayjs.utc().format("YYYY-MM-DD HH:mm:ss") }); return true; }; diff --git a/ui/src/repository/admin.ts b/ui/src/repository/admin.ts index 15074fee..da8ae5c3 100644 --- a/ui/src/repository/admin.ts +++ b/ui/src/repository/admin.ts @@ -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); }; diff --git a/ui/src/repository/certificate.ts b/ui/src/repository/certificate.ts index 6bfbbcbe..61de9c51 100644 --- a/ui/src/repository/certificate.ts +++ b/ui/src/repository/certificate.ts @@ -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(page, perPage, options); + return pb.collection(COLLECTION_NAME_CERTIFICATE).getList(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(options); + return { + totalItems: items.length, + items: items, + }; }; export const remove = async (record: MaybeModelRecordWithId) => { await getPocketBase() - .collection(COLLECTION_NAME) + .collection(COLLECTION_NAME_CERTIFICATE) .update(record.id!, { deleted: dayjs.utc().format("YYYY-MM-DD HH:mm:ss") }); return true; }; diff --git a/ui/src/repository/settings.ts b/ui/src/repository/settings.ts index 8985ae0f..df9bafe0 100644 --- a/ui/src/repository/settings.ts +++ b/ui/src/repository/settings.ts @@ -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 >(name: SettingsNames) => { try { - const resp = await getPocketBase().collection(COLLECTION_NAME).getFirstListItem>(`name='${name}'`, { + const resp = await getPocketBase().collection(COLLECTION_NAME_SETTINGS).getFirstListItem>(`name='${name}'`, { requestKey: null, }); return resp; @@ -25,8 +23,8 @@ export const get = async >(name: SettingsNames) = export const save = async >(record: MaybeModelRecordWithId>) => { if (record.id) { - return await getPocketBase().collection(COLLECTION_NAME).update>(record.id, record); + return await getPocketBase().collection(COLLECTION_NAME_SETTINGS).update>(record.id, record); } - return await getPocketBase().collection(COLLECTION_NAME).create>(record); + return await getPocketBase().collection(COLLECTION_NAME_SETTINGS).create>(record); }; diff --git a/ui/src/repository/workflow.ts b/ui/src/repository/workflow.ts index 54ddddd4..9b0f6ef5 100644 --- a/ui/src/repository/workflow.ts +++ b/ui/src/repository/workflow.ts @@ -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(page, perPage, options); + return await pb.collection(COLLECTION_NAME_WORKFLOW).getList(page, perPage, options); }; export const get = async (id: string) => { - return await getPocketBase().collection(COLLECTION_NAME).getOne(id, { + return await getPocketBase().collection(COLLECTION_NAME_WORKFLOW).getOne(id, { requestKey: null, }); }; @@ -38,25 +36,21 @@ export const get = async (id: string) => { export const save = async (record: MaybeModelRecord) => { if (record.id) { return await getPocketBase() - .collection(COLLECTION_NAME) + .collection(COLLECTION_NAME_WORKFLOW) .update(record.id as string, record); } - return await getPocketBase().collection(COLLECTION_NAME).create(record); + return await getPocketBase().collection(COLLECTION_NAME_WORKFLOW).create(record); }; export const remove = async (record: MaybeModelRecordWithId) => { - 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) => 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); }; diff --git a/ui/src/repository/workflowRun.ts b/ui/src/repository/workflowRun.ts index 0aa88080..4ede9a6f 100644 --- a/ui/src/repository/workflowRun.ts +++ b/ui/src/repository/workflowRun.ts @@ -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(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) => { - 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) => 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); }; diff --git a/ui/src/stores/workflow/index.ts b/ui/src/stores/workflow/index.ts index 4e1559ff..9ec83926 100644 --- a/ui/src/stores/workflow/index.ts +++ b/ui/src/stores/workflow/index.ts @@ -25,14 +25,14 @@ export type WorkflowState = { discard(): void; destroy(): void; - addNode: (node: WorkflowNode, preId: string) => void; + addNode: (node: WorkflowNode, previousNodeId: string) => void; updateNode: (node: WorkflowNode) => void; removeNode: (nodeId: string) => void; addBranch: (branchId: string) => void; removeBranch: (branchId: string, index: number) => void; - getWorkflowOuptutBeforeId: (id: string, type: string) => WorkflowNode[]; + getWorkflowOuptutBeforeId: (nodeId: string, type: string) => WorkflowNode[]; }; export const useWorkflowStore = create((set, get) => ({ @@ -143,10 +143,10 @@ export const useWorkflowStore = create((set, get) => ({ }); }, - addNode: async (node: WorkflowNode, preId: string) => { + addNode: async (node: WorkflowNode, previousNodeId: string) => { if (!get().initialized) throw "Workflow not initialized yet"; - const root = addNode(get().workflow.draft!, preId, node); + const root = addNode(get().workflow.draft!, previousNodeId, node); const resp = await saveWorkflow({ id: get().workflow.id!, draft: root, @@ -243,7 +243,7 @@ export const useWorkflowStore = create((set, get) => ({ }); }, - getWorkflowOuptutBeforeId: (id: string, type: string) => { - return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, id, type); + getWorkflowOuptutBeforeId: (nodeId: string, type: string) => { + return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, nodeId, type); }, })); diff --git a/ui/src/utils/cron.ts b/ui/src/utils/cron.ts index c2ca22e2..79a94ddc 100644 --- a/ui/src/utils/cron.ts +++ b/ui/src/utils/cron.ts @@ -3,6 +3,8 @@ export const validCronExpression = (expr: string): boolean => { try { parseExpression(expr); + + if (expr.trim().split(" ").length !== 5) return false; // pocketbase 后端仅支持五段式的表达式 return true; } catch { return false; @@ -10,19 +12,15 @@ export const validCronExpression = (expr: string): boolean => { }; export const getNextCronExecutions = (expr: string, times = 1): Date[] => { - if (!expr) return []; + if (!validCronExpression(expr)) return []; - try { - const now = new Date(); - const cron = parseExpression(expr, { currentDate: now, iterator: true }); + const now = new Date(); + const cron = parseExpression(expr, { currentDate: now, iterator: true }); - const result: Date[] = []; - for (let i = 0; i < times; i++) { - const next = cron.next(); - result.push(next.value.toDate()); - } - return result; - } catch { - return []; + const result: Date[] = []; + for (let i = 0; i < times; i++) { + const next = cron.next(); + result.push(next.value.toDate()); } + return result; }; diff --git a/ui/src/utils/error.ts b/ui/src/utils/error.ts index df5465f4..8ffb047a 100644 --- a/ui/src/utils/error.ts +++ b/ui/src/utils/error.ts @@ -7,13 +7,13 @@ export const getErrMsg = (error: unknown): string => { return error.message; } else if (typeof error === "object" && error != null) { if ("message" in error) { - return String(error.message); + return getErrMsg(error.message); } else if ("msg" in error) { - return String(error.msg); + return getErrMsg(error.msg); } } else if (typeof error === "string") { - return error; + return error || "Unknown error"; } - return String(error ?? "Unknown error"); + return "Unknown error"; };