diff --git a/go.mod b/go.mod index ff2e8913a..0498966db 100644 --- a/go.mod +++ b/go.mod @@ -44,6 +44,7 @@ require ( github.com/baidubce/bce-sdk-go v0.9.267 github.com/byteplus-sdk/byteplus-go-sdk-v2 v1.0.65 github.com/byteplus-sdk/byteplus-sdk-golang v1.0.65 + github.com/cloudflare/cloudflare-go/v7 v7.4.0 github.com/go-acme/lego/v5 v5.2.2 github.com/go-cmd/cmd v1.4.3 github.com/go-resty/resty/v2 v2.17.2 @@ -272,6 +273,7 @@ require ( github.com/rs/xid v1.6.0 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect github.com/zeebo/xxh3 v1.1.0 // indirect diff --git a/go.sum b/go.sum index f1b950713..883b60a5d 100644 --- a/go.sum +++ b/go.sum @@ -299,6 +299,8 @@ github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/cloudflare-go/v7 v7.4.0 h1:JdTxzeXcAhtJ9rUkNISK4ABA55pZP8HLxx6XsPSA7dU= +github.com/cloudflare/cloudflare-go/v7 v7.4.0/go.mod h1:9zcoIAtu6cmcoPszCNISvqYMXs8wObtVGXE1qGFMrNU= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= @@ -960,6 +962,7 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vod v1.3.96 h1:3fSwkZOA github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vod v1.3.96/go.mod h1:8PWWzrPRuy6iYbINeI/hd9kZk4p9Pm5vCOXMw8a8kZs= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/waf v1.3.95 h1:7kkSVeUUOi1Sc7K/neN7y3qhdebQrQOqs0HScHBDGOg= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/waf v1.3.95/go.mod h1:JHnW2i0WR2wq62Z41prri06y5pLMZon7TavXGVpnYfQ= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= @@ -968,6 +971,8 @@ github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= diff --git a/internal/certmgmt/deployers/sp_cloudflare_ssl.go b/internal/certmgmt/deployers/sp_cloudflare_ssl.go new file mode 100644 index 000000000..9c554729d --- /dev/null +++ b/internal/certmgmt/deployers/sp_cloudflare_ssl.go @@ -0,0 +1,27 @@ +package deployers + +import ( + "fmt" + + "github.com/certimate-go/certimate/internal/domain" + "github.com/certimate-go/certimate/pkg/core" + dplyimpl "github.com/certimate-go/certimate/pkg/core/deployer/providers/cloudflare-ssl" + xmaps "github.com/certimate-go/certimate/pkg/utils/maps" +) + +func init() { + Registries.MustRegister(domain.DeploymentProviderTypeCloudflareSSL, func(options *ProviderFactoryOptions) (core.Deployer, error) { + credentials := domain.AccessConfigForCloudflare{} + if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + provider, err := dplyimpl.NewDeployer(&dplyimpl.DeployerConfig{ + ApiToken: credentials.ApiToken, + Environment: xmaps.GetString(options.ProviderExtendedConfig, "environment"), + ZoneId: xmaps.GetString(options.ProviderExtendedConfig, "zoneId"), + CertificateId: xmaps.GetString(options.ProviderExtendedConfig, "certificateId"), + }) + return provider, err + }) +} diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 94c28e8a2..407332d15 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -355,6 +355,7 @@ const ( DeploymentProviderTypeBytePlusTOS = DeploymentProviderType(AccessProviderTypeBytePlus + "-tos") DeploymentProviderTypeCacheFly = DeploymentProviderType(AccessProviderTypeCacheFly) DeploymentProviderTypeCdnfly = DeploymentProviderType(AccessProviderTypeCdnfly) + DeploymentProviderTypeCloudflareSSL = DeploymentProviderType(AccessProviderTypeCloudflare + "-ssl") DeploymentProviderTypeCPanel = DeploymentProviderType(AccessProviderTypeCPanel) DeploymentProviderTypeCTCCCloudAO = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-ao") DeploymentProviderTypeCTCCCloudCDN = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-cdn") diff --git a/pkg/core/deployer/providers/cloudflare-ssl/cloudflare_ssl.go b/pkg/core/deployer/providers/cloudflare-ssl/cloudflare_ssl.go new file mode 100644 index 000000000..00bfcdfe5 --- /dev/null +++ b/pkg/core/deployer/providers/cloudflare-ssl/cloudflare_ssl.go @@ -0,0 +1,119 @@ +package cloudflaressl + +import ( + "cmp" + "context" + "fmt" + "log/slog" + + cf "github.com/cloudflare/cloudflare-go/v7" + cfcertificates "github.com/cloudflare/cloudflare-go/v7/custom_certificates" + cfhostnames "github.com/cloudflare/cloudflare-go/v7/custom_hostnames" + cfoption "github.com/cloudflare/cloudflare-go/v7/option" + + "github.com/certimate-go/certimate/pkg/core" +) + +type ( + Provider = core.Deployer + DeployResult = core.DeployerDeployResult +) + +type DeployerConfig struct { + // Cloudflare API Token。 + ApiToken string `json:"apiToken"` + // Cloudflare 环境。 + // 选填。 + // 零值时默认值 "production"。 + Environment string `json:"environment,omitempty"` + // Cloudflare DNS 区域 ID。 + ZoneId string `json:"zoneId"` + // Cloudflare 证书 ID。 + // 选填。零值时表示新建证书;否则表示更新证书。 + CertificateId string `json:"certificateId,omitempty"` +} + +type Deployer struct { + config *DeployerConfig + logger *slog.Logger + sdkClient *cfcertificates.CustomCertificateService +} + +var _ Provider = (*Deployer)(nil) + +func NewDeployer(config *DeployerConfig) (*Deployer, error) { + if config == nil { + return nil, fmt.Errorf("the configuration of the deployer provider is nil") + } + + client, err := createSDKClient(config.ApiToken) + if err != nil { + return nil, fmt.Errorf("could not create client: %w", err) + } + + return &Deployer{ + config: config, + logger: slog.Default(), + sdkClient: client, + }, nil +} + +func (d *Deployer) SetLogger(logger *slog.Logger) { + if logger == nil { + d.logger = slog.New(slog.DiscardHandler) + } else { + d.logger = logger + } +} + +func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*DeployResult, error) { + if d.config.ZoneId == "" { + return nil, fmt.Errorf("config `zoneId` is required") + } + + if d.config.CertificateId == "" { + // 新建自定义证书 + // REF: https://developers.cf.com/api/resources/custom_certificates/methods/create + customCertificateNewReq := cfcertificates.CustomCertificateNewParams{ + ZoneID: cf.F(d.config.ZoneId), + Certificate: cf.F(certPEM), + PrivateKey: cf.F(privkeyPEM), + BundleMethod: cf.F(cfhostnames.BundleMethodUbiquitous), + Deploy: cf.F(cfcertificates.CustomCertificateNewParamsDeploy(cmp.Or(d.config.Environment, "production"))), + } + customCertificateNewResp, err := d.sdkClient.New(ctx, customCertificateNewReq) + d.logger.Debug("sdk request 'CustomCertificates.New'", slog.Any("request", customCertificateNewReq), slog.Any("response", customCertificateNewResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'CustomCertificates.New': %w", err) + } + } else { + // 编辑自定义证书 + // REF: https://developers.cloudflare.com/api/resources/custom_certificates/methods/edit + customCertificateEditReq := cfcertificates.CustomCertificateEditParams{ + ZoneID: cf.F(d.config.ZoneId), + Certificate: cf.F(certPEM), + PrivateKey: cf.F(privkeyPEM), + BundleMethod: cf.F(cfhostnames.BundleMethodUbiquitous), + Deploy: cf.F(cfcertificates.CustomCertificateEditParamsDeploy(cmp.Or(d.config.Environment, "production"))), + } + customCertificateEditResp, err := d.sdkClient.Edit(ctx, d.config.CertificateId, customCertificateEditReq) + d.logger.Debug("sdk request 'CustomCertificates.Edit'", slog.Any("request", customCertificateEditReq), slog.Any("response", customCertificateEditResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'CustomCertificates.Edit': %w", err) + } + } + + return &DeployResult{}, nil +} + +func createSDKClient(apiToken string) (*cfcertificates.CustomCertificateService, error) { + if apiToken == "" { + return nil, fmt.Errorf("cloudflare: invalid api token") + } + + opts := append(cf.DefaultClientOptions(), cfoption.WithAPIToken(apiToken)) + + srv := cfcertificates.NewCustomCertificateService(opts...) + + return srv, nil +} diff --git a/pkg/core/deployer/providers/cloudflare-ssl/cloudflare_ssl_test.go b/pkg/core/deployer/providers/cloudflare-ssl/cloudflare_ssl_test.go new file mode 100644 index 000000000..90a1567f5 --- /dev/null +++ b/pkg/core/deployer/providers/cloudflare-ssl/cloudflare_ssl_test.go @@ -0,0 +1,53 @@ +package cloudflaressl_test + +import ( + "testing" + + "github.com/certimate-go/certimate/pkg/core/deployer/internal/tester" + impl "github.com/certimate-go/certimate/pkg/core/deployer/providers/cloudflare-ssl" +) + +var ( + fp = tester.Args("CLOUDFLARESSL_") + fTestCertPath string + fTestKeyPath string + fApiToken string + fZoneId string + fCertificateId string +) + +func init() { + fp.DefineString(&fTestCertPath, "TESTCERTPATH") + fp.DefineString(&fTestKeyPath, "TESTKEYPATH") + fp.DefineString(&fApiToken, "APITOKEN") + fp.DefineString(&fZoneId, "ZONEID") + fp.DefineString(&fCertificateId, "CERTIFICATEID") +} + +/* +Shell command to run this test: + + go test -v ./cloudflare_ssl_test.go -args \ + --CLOUDFLARESSL_TESTCERTPATH="/path/to/your-test-cert.pem" \ + --CLOUDFLARESSL_TESTKEYPATH="/path/to/your-test-key.pem" \ + --CLOUDFLARESSL_APITOKEN="your-api-token" \ + --CLOUDFLARESSL_ZONEID="your-zone-id" \ + --CLOUDFLARESSL_CERTIFICATEID="your-certificate-id" +*/ +func TestProvider(t *testing.T) { + fp.Parse() + + t.Run("Deploy", func(t *testing.T) { + provider, err := impl.NewDeployer(&impl.DeployerConfig{ + ApiToken: fApiToken, + ZoneId: fZoneId, + CertificateId: fCertificateId, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + tester.TestDeploy(t, provider, tester.TestDeployArgs{CertPath: fTestCertPath, KeyPath: fTestKeyPath}) + }) +} diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx index 9bd5b5e81..e2eb3b942 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx @@ -50,6 +50,7 @@ import BizDeployNodeConfigFieldsProviderBytePlusCLB from "./BizDeployNodeConfigF import BizDeployNodeConfigFieldsProviderBytePlusMediaLive from "./BizDeployNodeConfigFieldsProviderBytePlusMediaLive"; import BizDeployNodeConfigFieldsProviderBytePlusTOS from "./BizDeployNodeConfigFieldsProviderBytePlusTOS"; import BizDeployNodeConfigFieldsProviderCdnfly from "./BizDeployNodeConfigFieldsProviderCdnfly"; +import BizDeployNodeConfigFieldsProviderCloudflareSSL from "./BizDeployNodeConfigFieldsProviderCloudflareSSL"; import BizDeployNodeConfigFieldsProviderCPanel from "./BizDeployNodeConfigFieldsProviderCPanel"; import BizDeployNodeConfigFieldsProviderCTCCCloudAO from "./BizDeployNodeConfigFieldsProviderCTCCCloudAO"; import BizDeployNodeConfigFieldsProviderCTCCCloudCDN from "./BizDeployNodeConfigFieldsProviderCTCCCloudCDN"; @@ -189,6 +190,7 @@ const providerComponentMap: Partial { + const { i18n, t } = useTranslation(); + + const { parentNamePath } = useFormNestedFieldsContext(); + const formSchema = z.object({ + [parentNamePath]: getSchema({ i18n }), + }); + const formRule = createSchemaFieldRule(formSchema); + const initialValues = getInitialValues(); + + return ( + <> + } + > + ({ value: s }))} + placeholder={t("workflow_node.deploy.form.cloudflare_ssl_environment.placeholder")} + /> + + + } + > + + + + } + > + + + + ); +}; + +const getInitialValues = (): Nullish>> => { + return { + environment: "production", + zoneId: "", + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { + const { t: _ } = i18n; + + return z.object({ + environment: z.string().nullish(), + zoneId: z.string().nonempty(), + certificateId: z.string().nullish(), + }); +}; + +const _default = Object.assign(BizDeployNodeConfigFieldsProviderCloudflareSSL, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderWangsuCDNPro.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderWangsuCDNPro.tsx index c6e452aed..f060c9caa 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderWangsuCDNPro.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderWangsuCDNPro.tsx @@ -31,7 +31,7 @@ const BizDeployNodeConfigFieldsProviderWangsuCDNPro = () => { rules={[formRule]} >