Repository: certimate-go/certimate Branch: main Commit: 30552d3313d1 Files: 1663 Total size: 4.7 MB Directory structure: gitextract_ubpd6109/ ├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── 1-bug_report.yml │ │ ├── 2-feature_request.yml │ │ ├── 3-questions.yml │ │ └── config.yml │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ ├── push_image.yml │ ├── push_image_next.yml │ ├── release.yml │ ├── release_sync_gitee.py │ └── release_sync_gitee.yml ├── .gitignore ├── .goreleaser.yml ├── .vscode/ │ ├── extensions.json │ ├── settings.json │ └── settings.tailwind.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── CONTRIBUTING_zh.md ├── Dockerfile ├── Dockerfile.gh ├── LICENSE ├── Makefile ├── README.md ├── README_zh.md ├── cmd/ │ ├── intercmd.go │ ├── serve_nonwindows.go │ ├── serve_windows.go │ ├── winsc_nonwindows.go │ └── winsc_windows.go ├── docker/ │ └── docker-compose.yml ├── go.mod ├── go.sum ├── internal/ │ ├── app/ │ │ ├── app.go │ │ ├── scheduler.go │ │ └── singleton.go │ ├── certacme/ │ │ ├── account.go │ │ ├── certifiers/ │ │ │ ├── registry.go │ │ │ ├── sp_35cn.go │ │ │ ├── sp_51dnscom.go │ │ │ ├── sp_acmedns.go │ │ │ ├── sp_acmehttpreq.go │ │ │ ├── sp_akamai_edgedns.go │ │ │ ├── sp_aliyun_dns.go │ │ │ ├── sp_aliyun_esa.go │ │ │ ├── sp_arvancloud.go │ │ │ ├── sp_aws_route53.go │ │ │ ├── sp_azure_dns.go │ │ │ ├── sp_baiducloud_dns.go │ │ │ ├── sp_bookmyname.go │ │ │ ├── sp_bunny.go │ │ │ ├── sp_cloudflare.go │ │ │ ├── sp_cloudns.go │ │ │ ├── sp_cmcccloud_dns.go │ │ │ ├── sp_constellix.go │ │ │ ├── sp_cpanel.go │ │ │ ├── sp_ctcccloud_smartdns.go │ │ │ ├── sp_desec.go │ │ │ ├── sp_digitalocean.go │ │ │ ├── sp_dnsexit.go │ │ │ ├── sp_dnsla.go │ │ │ ├── sp_dnsmadeeasy.go │ │ │ ├── sp_duckdns.go │ │ │ ├── sp_dynu.go │ │ │ ├── sp_dynv6.go │ │ │ ├── sp_gandinet.go │ │ │ ├── sp_gcore.go │ │ │ ├── sp_gname.go │ │ │ ├── sp_godaddy.go │ │ │ ├── sp_hetzner.go │ │ │ ├── sp_hostingde.go │ │ │ ├── sp_hostinger.go │ │ │ ├── sp_huaweicloud_dns.go │ │ │ ├── sp_infomaniak.go │ │ │ ├── sp_ionos.go │ │ │ ├── sp_jdcloud_dns.go │ │ │ ├── sp_linode.go │ │ │ ├── sp_local.go │ │ │ ├── sp_namecheap.go │ │ │ ├── sp_namedotcom.go │ │ │ ├── sp_namesilo.go │ │ │ ├── sp_netcup.go │ │ │ ├── sp_netlify.go │ │ │ ├── sp_ns1.go │ │ │ ├── sp_ovhcloud.go │ │ │ ├── sp_porkbun.go │ │ │ ├── sp_powerdns.go │ │ │ ├── sp_qingcloud_dns.go │ │ │ ├── sp_rainyun.go │ │ │ ├── sp_rfc2136.go │ │ │ ├── sp_s3.go │ │ │ ├── sp_spaceship.go │ │ │ ├── sp_ssh.go │ │ │ ├── sp_technitiumdns.go │ │ │ ├── sp_tencentcloud_dns.go │ │ │ ├── sp_tencentcloud_eo.go │ │ │ ├── sp_todaynic.go │ │ │ ├── sp_ucloud_udnr.go │ │ │ ├── sp_vercel.go │ │ │ ├── sp_volcengine_dns.go │ │ │ ├── sp_vultr.go │ │ │ ├── sp_westcn.go │ │ │ └── sp_xinnet.go │ │ ├── client.go │ │ ├── client_obtain.go │ │ ├── client_revoke.go │ │ ├── config.go │ │ └── logging.go │ ├── certificate/ │ │ ├── service.go │ │ └── service_deps.go │ ├── certmgmt/ │ │ ├── client.go │ │ ├── client_deploy.go │ │ └── deployers/ │ │ ├── registry.go │ │ ├── sp_1panel.go │ │ ├── sp_1panel_console.go │ │ ├── sp_aliyun_alb.go │ │ ├── sp_aliyun_apigw.go │ │ ├── sp_aliyun_cas.go │ │ ├── sp_aliyun_casdeploy.go │ │ ├── sp_aliyun_cdn.go │ │ ├── sp_aliyun_clb.go │ │ ├── sp_aliyun_dcdn.go │ │ ├── sp_aliyun_ddospro.go │ │ ├── sp_aliyun_esa.go │ │ ├── sp_aliyun_esasaas.go │ │ ├── sp_aliyun_fc.go │ │ ├── sp_aliyun_ga.go │ │ ├── sp_aliyun_live.go │ │ ├── sp_aliyun_nlb.go │ │ ├── sp_aliyun_oss.go │ │ ├── sp_aliyun_vod.go │ │ ├── sp_aliyun_waf.go │ │ ├── sp_apisix.go │ │ ├── sp_aws_acm.go │ │ ├── sp_aws_cloudfront.go │ │ ├── sp_aws_iam.go │ │ ├── sp_azure_keyvault.go │ │ ├── sp_baiducloud_appblb.go │ │ ├── sp_baiducloud_blb.go │ │ ├── sp_baiducloud_cdn.go │ │ ├── sp_baiducloud_cert.go │ │ ├── sp_baishan_cdn.go │ │ ├── sp_baotapanel.go │ │ ├── sp_baotapanel_console.go │ │ ├── sp_baotapanelgo.go │ │ ├── sp_baotapanelgo_console.go │ │ ├── sp_baotawaf.go │ │ ├── sp_baotawaf_console.go │ │ ├── sp_bunny_cdn.go │ │ ├── sp_byteplus_cdn.go │ │ ├── sp_cachefly.go │ │ ├── sp_cdnfly.go │ │ ├── sp_cpanel.go │ │ ├── sp_ctcccloud_ao.go │ │ ├── sp_ctcccloud_cdn.go │ │ ├── sp_ctcccloud_cms.go │ │ ├── sp_ctcccloud_elb.go │ │ ├── sp_ctcccloud_faas.go │ │ ├── sp_ctcccloud_icdn.go │ │ ├── sp_ctcccloud_lvdn.go │ │ ├── sp_dogecloud_cdn.go │ │ ├── sp_dokploy.go │ │ ├── sp_flexcdn.go │ │ ├── sp_flyio.go │ │ ├── sp_gcore_cdn.go │ │ ├── sp_goedge.go │ │ ├── sp_huaweicloud_cdn.go │ │ ├── sp_huaweicloud_elb.go │ │ ├── sp_huaweicloud_obs.go │ │ ├── sp_huaweicloud_scm.go │ │ ├── sp_huaweicloud_waf.go │ │ ├── sp_jdcloud_alb.go │ │ ├── sp_jdcloud_cdn.go │ │ ├── sp_jdcloud_live.go │ │ ├── sp_jdcloud_vod.go │ │ ├── sp_kong.go │ │ ├── sp_ksyun_cdn.go │ │ ├── sp_kubernetes_secret.go │ │ ├── sp_lecdn.go │ │ ├── sp_local.go │ │ ├── sp_mohua_mvh.go │ │ ├── sp_netlify.go │ │ ├── sp_nginxproxymanager.go │ │ ├── sp_proxmoxve.go │ │ ├── sp_qiniu_cdn.go │ │ ├── sp_qiniu_kodo.go │ │ ├── sp_qiniu_pili.go │ │ ├── sp_rainyun_rcdn.go │ │ ├── sp_rainyun_sslcenter.go │ │ ├── sp_ratpanel.go │ │ ├── sp_ratpanel_console.go │ │ ├── sp_s3.go │ │ ├── sp_safeline.go │ │ ├── sp_ssh.go │ │ ├── sp_synologydsm.go │ │ ├── sp_tencentcloud_cdn.go │ │ ├── sp_tencentcloud_clb.go │ │ ├── sp_tencentcloud_cos.go │ │ ├── sp_tencentcloud_css.go │ │ ├── sp_tencentcloud_ecdn.go │ │ ├── sp_tencentcloud_eo.go │ │ ├── sp_tencentcloud_gaap.go │ │ ├── sp_tencentcloud_scf.go │ │ ├── sp_tencentcloud_ssl.go │ │ ├── sp_tencentcloud_ssldeploy.go │ │ ├── sp_tencentcloud_sslupdate.go │ │ ├── sp_tencentcloud_vod.go │ │ ├── sp_tencentcloud_waf.go │ │ ├── sp_ucloud_ualb.go │ │ ├── sp_ucloud_ucdn.go │ │ ├── sp_ucloud_uclb.go │ │ ├── sp_ucloud_uewaf.go │ │ ├── sp_ucloud_upathx.go │ │ ├── sp_ucloud_us3.go │ │ ├── sp_unicloud_webhost.go │ │ ├── sp_upyun_cdn.go │ │ ├── sp_upyun_file.go │ │ ├── sp_volcengine_alb.go │ │ ├── sp_volcengine_cdn.go │ │ ├── sp_volcengine_certcenter.go │ │ ├── sp_volcengine_clb.go │ │ ├── sp_volcengine_dcdn.go │ │ ├── sp_volcengine_imagex.go │ │ ├── sp_volcengine_live.go │ │ ├── sp_volcengine_tos.go │ │ ├── sp_volcengine_vod.go │ │ ├── sp_volcengine_waf.go │ │ ├── sp_wangsu_cdn.go │ │ ├── sp_wangsu_cdnpro.go │ │ ├── sp_wangsu_certificate.go │ │ └── sp_webhook.go │ ├── domain/ │ │ ├── access.go │ │ ├── acme_account.go │ │ ├── certificate.go │ │ ├── dtos/ │ │ │ ├── certificate.go │ │ │ ├── notify.go │ │ │ └── workflow.go │ │ ├── error.go │ │ ├── expr/ │ │ │ ├── expr.go │ │ │ └── expr_test.go │ │ ├── meta.go │ │ ├── provider.go │ │ ├── settings.go │ │ ├── statistics.go │ │ ├── workflow.go │ │ ├── workflow_log.go │ │ ├── workflow_output.go │ │ └── workflow_run.go │ ├── notify/ │ │ ├── client.go │ │ ├── client_notifier.go │ │ ├── notifiers/ │ │ │ ├── registry.go │ │ │ ├── sp_dingtalkbot.go │ │ │ ├── sp_discordbot.go │ │ │ ├── sp_email.go │ │ │ ├── sp_larkbot.go │ │ │ ├── sp_mattermost.go │ │ │ ├── sp_slackbot.go │ │ │ ├── sp_telegrambot.go │ │ │ ├── sp_webhook.go │ │ │ └── sp_wecombot.go │ │ ├── service.go │ │ └── service_deps.go │ ├── repository/ │ │ ├── access.go │ │ ├── acme_account.go │ │ ├── certificate.go │ │ ├── settings.go │ │ ├── statistics.go │ │ ├── workflow.go │ │ ├── workflow_log.go │ │ ├── workflow_output.go │ │ └── workflow_run.go │ ├── rest/ │ │ ├── handlers/ │ │ │ ├── certificates.go │ │ │ ├── notifications.go │ │ │ ├── statistics.go │ │ │ └── workflows.go │ │ ├── resp/ │ │ │ └── resp.go │ │ └── routes/ │ │ └── routes.go │ ├── scheduler/ │ │ ├── certificate.go │ │ ├── scheduler.go │ │ └── workflow.go │ ├── statistics/ │ │ ├── service.go │ │ └── service_deps.go │ ├── tools/ │ │ ├── mproc/ │ │ │ ├── receiver.go │ │ │ └── sender.go │ │ ├── s3/ │ │ │ ├── client.go │ │ │ └── config.go │ │ ├── smtp/ │ │ │ ├── client.go │ │ │ ├── config.go │ │ │ ├── errhandler.go │ │ │ └── message.go │ │ └── ssh/ │ │ ├── auth.go │ │ ├── client.go │ │ └── config.go │ └── workflow/ │ ├── dispatcher/ │ │ ├── deps.go │ │ ├── dispatcher.go │ │ ├── singleton.go │ │ └── task.go │ ├── engine/ │ │ ├── context.go │ │ ├── deps.go │ │ ├── engine.go │ │ ├── errors.go │ │ ├── executor.go │ │ ├── executor_bizapply.go │ │ ├── executor_bizdeploy.go │ │ ├── executor_bizmonitor.go │ │ ├── executor_biznotify.go │ │ ├── executor_bizupload.go │ │ ├── executor_condition.go │ │ ├── executor_delay.go │ │ ├── executor_end.go │ │ ├── executor_start.go │ │ ├── executor_trycatch.go │ │ ├── logger.go │ │ ├── models.go │ │ └── state.go │ ├── pbhook.go │ ├── pbjob.go │ ├── service.go │ ├── service_deps.go │ ├── service_inst.go │ └── workflow.go ├── main.go ├── migrations/ │ ├── 1757476800_upgrade_v0.4.0.go │ ├── 1757476801_initialize_v0.4.0.go │ ├── 1760486400_upgrade_v0.4.1.go │ ├── 1762142400_upgrade_v0.4.3.go │ ├── 1762516800_upgrade_v0.4.4.go │ ├── 1763373600_upgrade_v0.4.5.go │ ├── 1763640000_upgrade_v0.4.6.go │ ├── 1766592000_upgrade_v0.4.11.go │ ├── 1766800800_upgrade_v0.4.12.go │ ├── 1767024000_upgrade_v0.4.13.go │ ├── 1768363200_upgrade_v0.4.14.go │ ├── 1769313600_upgrade_v0.4.15.go │ ├── snaps/ │ │ ├── v0.3/ │ │ │ └── workflow.go │ │ └── v0.4/ │ │ └── workflow.go │ └── tracer.go ├── pkg/ │ ├── core/ │ │ ├── certifier/ │ │ │ ├── challenger.go │ │ │ └── challengers/ │ │ │ ├── dns01/ │ │ │ │ ├── 35cn/ │ │ │ │ │ └── 35cn.go │ │ │ │ ├── 51dnscom/ │ │ │ │ │ ├── 51dnscom.go │ │ │ │ │ └── internal/ │ │ │ │ │ └── lego.go │ │ │ │ ├── acmedns/ │ │ │ │ │ └── acmedns.go │ │ │ │ ├── acmehttpreq/ │ │ │ │ │ └── acmehttpreq.go │ │ │ │ ├── akamai-edgedns/ │ │ │ │ │ └── akamai_edgedns.go │ │ │ │ ├── aliyun/ │ │ │ │ │ └── aliyun.go │ │ │ │ ├── aliyun-esa/ │ │ │ │ │ └── aliyun_esa.go │ │ │ │ ├── arvancloud/ │ │ │ │ │ └── arvancloud.go │ │ │ │ ├── aws-route53/ │ │ │ │ │ └── aws-route53.go │ │ │ │ ├── azure-dns/ │ │ │ │ │ └── azure-dns.go │ │ │ │ ├── baiducloud/ │ │ │ │ │ └── baiducloud.go │ │ │ │ ├── bookmyname/ │ │ │ │ │ └── bookmyname.go │ │ │ │ ├── bunny/ │ │ │ │ │ └── bunny.go │ │ │ │ ├── cloudflare/ │ │ │ │ │ └── cloudflare.go │ │ │ │ ├── cloudns/ │ │ │ │ │ └── cloudns.go │ │ │ │ ├── cmcccloud/ │ │ │ │ │ ├── cmcccloud.go │ │ │ │ │ └── internal/ │ │ │ │ │ └── lego.go │ │ │ │ ├── constellix/ │ │ │ │ │ └── constellix.go │ │ │ │ ├── cpanel/ │ │ │ │ │ └── cpanel.go │ │ │ │ ├── ctcccloud/ │ │ │ │ │ ├── ctcccloud.go │ │ │ │ │ └── internal/ │ │ │ │ │ └── lego.go │ │ │ │ ├── desec/ │ │ │ │ │ └── desec.go │ │ │ │ ├── digitalocean/ │ │ │ │ │ └── digitalocean.go │ │ │ │ ├── dnsexit/ │ │ │ │ │ └── dnsexit.go │ │ │ │ ├── dnsla/ │ │ │ │ │ ├── dnsla.go │ │ │ │ │ └── internal/ │ │ │ │ │ └── lego.go │ │ │ │ ├── dnsmadeeasy/ │ │ │ │ │ └── dnsmadeeasy.go │ │ │ │ ├── duckdns/ │ │ │ │ │ └── duckdns.go │ │ │ │ ├── dynu/ │ │ │ │ │ └── dynu.go │ │ │ │ ├── dynv6/ │ │ │ │ │ ├── dynv6.go │ │ │ │ │ └── internal/ │ │ │ │ │ └── lego.go │ │ │ │ ├── gandinet/ │ │ │ │ │ └── gandinet.go │ │ │ │ ├── gcore/ │ │ │ │ │ └── gcore.go │ │ │ │ ├── gname/ │ │ │ │ │ ├── gname.go │ │ │ │ │ └── internal/ │ │ │ │ │ └── lego.go │ │ │ │ ├── godaddy/ │ │ │ │ │ └── godaddy.go │ │ │ │ ├── hetzner/ │ │ │ │ │ └── hetzner.go │ │ │ │ ├── hostingde/ │ │ │ │ │ └── hostingde.go │ │ │ │ ├── hostinger/ │ │ │ │ │ └── hostinger.go │ │ │ │ ├── huaweicloud/ │ │ │ │ │ └── huaweicloud.go │ │ │ │ ├── infomaniak/ │ │ │ │ │ └── infomaniak.go │ │ │ │ ├── ionos/ │ │ │ │ │ └── ionos.go │ │ │ │ ├── jdcloud/ │ │ │ │ │ └── jdcloud.go │ │ │ │ ├── linode/ │ │ │ │ │ └── linode.go │ │ │ │ ├── namecheap/ │ │ │ │ │ └── namecheap.go │ │ │ │ ├── namedotcom/ │ │ │ │ │ └── namedotcom.go │ │ │ │ ├── namesilo/ │ │ │ │ │ └── namesilo.go │ │ │ │ ├── netcup/ │ │ │ │ │ └── netcup.go │ │ │ │ ├── netlify/ │ │ │ │ │ └── netlify.go │ │ │ │ ├── ns1/ │ │ │ │ │ └── ns1.go │ │ │ │ ├── ovhcloud/ │ │ │ │ │ ├── consts.go │ │ │ │ │ └── ovhcloud.go │ │ │ │ ├── porkbun/ │ │ │ │ │ └── porkbun.go │ │ │ │ ├── powerdns/ │ │ │ │ │ └── powerdns.go │ │ │ │ ├── qingcloud/ │ │ │ │ │ ├── internal/ │ │ │ │ │ │ └── lego.go │ │ │ │ │ └── qingcloud.go │ │ │ │ ├── rainyun/ │ │ │ │ │ └── rainyun.go │ │ │ │ ├── rfc2136/ │ │ │ │ │ └── rfc2136.go │ │ │ │ ├── spaceship/ │ │ │ │ │ └── spaceship.go │ │ │ │ ├── technitiumdns/ │ │ │ │ │ └── technitiumdns.go │ │ │ │ ├── tencentcloud/ │ │ │ │ │ └── tencentcloud.go │ │ │ │ ├── tencentcloud-eo/ │ │ │ │ │ └── tencentcloud_eo.go │ │ │ │ ├── todaynic/ │ │ │ │ │ └── todaynic.go │ │ │ │ ├── ucloud/ │ │ │ │ │ ├── internal/ │ │ │ │ │ │ └── lego.go │ │ │ │ │ └── ucloud.go │ │ │ │ ├── vercel/ │ │ │ │ │ └── vercel.go │ │ │ │ ├── volcengine/ │ │ │ │ │ └── volcengine.go │ │ │ │ ├── vultr/ │ │ │ │ │ └── vultr.go │ │ │ │ ├── westcn/ │ │ │ │ │ └── westcn.go │ │ │ │ └── xinnet/ │ │ │ │ ├── internal/ │ │ │ │ │ └── lego.go │ │ │ │ └── xinnet.go │ │ │ └── http01/ │ │ │ ├── local/ │ │ │ │ └── local.go │ │ │ ├── s3/ │ │ │ │ └── s3.go │ │ │ └── ssh/ │ │ │ └── ssh.go │ │ ├── certmgr/ │ │ │ ├── errors.go │ │ │ ├── provider.go │ │ │ └── providers/ │ │ │ ├── 1panel/ │ │ │ │ ├── 1panel.go │ │ │ │ └── 1panel_test.go │ │ │ ├── aliyun-cas/ │ │ │ │ ├── aliyun_cas.go │ │ │ │ ├── aliyun_cas_test.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── aliyun-slb/ │ │ │ │ ├── aliyun_slb.go │ │ │ │ ├── aliyun_slb_test.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── aws-acm/ │ │ │ │ └── aws_acm.go │ │ │ ├── aws-iam/ │ │ │ │ └── aws_iam.go │ │ │ ├── azure-keyvault/ │ │ │ │ ├── azure_keyvault.go │ │ │ │ └── azure_keyvault_test.go │ │ │ ├── baiducloud-cert/ │ │ │ │ ├── baiducloud_cert.go │ │ │ │ └── baiducloud_cert_test.go │ │ │ ├── baishan-cdn/ │ │ │ │ ├── baishan_cdn.go │ │ │ │ └── baishan_cdn_test.go │ │ │ ├── byteplus-cdn/ │ │ │ │ └── byteplus_cdn.go │ │ │ ├── ctcccloud-ao/ │ │ │ │ ├── ctcccloud_ao.go │ │ │ │ └── ctcccloud_ao_test.go │ │ │ ├── ctcccloud-cdn/ │ │ │ │ ├── ctcccloud_cdn.go │ │ │ │ └── ctcccloud_cdn_test.go │ │ │ ├── ctcccloud-cms/ │ │ │ │ ├── ctcccloud_cms.go │ │ │ │ └── ctcccloud_cms_test.go │ │ │ ├── ctcccloud-elb/ │ │ │ │ ├── ctcccloud_elb.go │ │ │ │ └── ctcccloud_elb_test.go │ │ │ ├── ctcccloud-icdn/ │ │ │ │ ├── ctcccloud_icdn.go │ │ │ │ └── ctcccloud_icdn_test.go │ │ │ ├── ctcccloud-lvdn/ │ │ │ │ ├── ctcccloud_lvdn.go │ │ │ │ └── ctcccloud_lvdn_test.go │ │ │ ├── dogecloud/ │ │ │ │ └── dogecloud.go │ │ │ ├── dokploy/ │ │ │ │ ├── dokploy.go │ │ │ │ └── dokploy_test.go │ │ │ ├── gcore-cdn/ │ │ │ │ └── gcore_cdn.go │ │ │ ├── huaweicloud-elb/ │ │ │ │ ├── huaweicloud_elb.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── huaweicloud-scm/ │ │ │ │ ├── huaweicloud_scm.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── huaweicloud-waf/ │ │ │ │ ├── huaweicloud_waf.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── jdcloud-ssl/ │ │ │ │ ├── jdcloud_ssl.go │ │ │ │ └── jdcloud_ssl_test.go │ │ │ ├── nginxproxymanager/ │ │ │ │ ├── consts.go │ │ │ │ ├── nginxproxymanager.go │ │ │ │ └── nginxproxymanager_test.go │ │ │ ├── qiniu-sslcert/ │ │ │ │ ├── qiniu_sslcert.go │ │ │ │ └── qiniu_sslcert_test.go │ │ │ ├── rainyun-sslcenter/ │ │ │ │ ├── rainyun_sslcenter.go │ │ │ │ └── rainyun_sslcenter_test.go │ │ │ ├── tencentcloud-ssl/ │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ ├── tencentcloud_ssl.go │ │ │ │ └── tencentcloud_ssl_test.go │ │ │ ├── ucloud-ulb/ │ │ │ │ ├── ucloud_ulb.go │ │ │ │ └── ucloud_ulb_test.go │ │ │ ├── ucloud-upathx/ │ │ │ │ ├── ucloud_upathx.go │ │ │ │ └── ucloud_upathx_test.go │ │ │ ├── ucloud-ussl/ │ │ │ │ ├── ucloud_ussl.go │ │ │ │ └── ucloud_ussl_test.go │ │ │ ├── upyun-ssl/ │ │ │ │ ├── upyun_ssl.go │ │ │ │ └── upyun_ssl_test.go │ │ │ ├── volcengine-cdn/ │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ └── volcengine_cdn.go │ │ │ ├── volcengine-certcenter/ │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ ├── volcengine_certcenter.go │ │ │ │ └── volcengine_certcenter_test.go │ │ │ ├── volcengine-live/ │ │ │ │ └── volcengine_live.go │ │ │ └── wangsu-certificate/ │ │ │ ├── wangsu_certificate.go │ │ │ └── wangsu_certificate_test.go │ │ ├── deployer/ │ │ │ ├── provider.go │ │ │ └── providers/ │ │ │ ├── 1panel/ │ │ │ │ ├── 1panel.go │ │ │ │ ├── 1panel_test.go │ │ │ │ └── consts.go │ │ │ ├── 1panel-console/ │ │ │ │ ├── 1panel_console.go │ │ │ │ └── 1panel_console_test.go │ │ │ ├── aliyun-alb/ │ │ │ │ ├── aliyun_alb.go │ │ │ │ ├── aliyun_alb_test.go │ │ │ │ ├── consts.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── aliyun-apigw/ │ │ │ │ ├── aliyun_apigw.go │ │ │ │ ├── aliyun_apigw_test.go │ │ │ │ ├── consts.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── aliyun-cas/ │ │ │ │ └── aliyun_cas.go │ │ │ ├── aliyun-cas-deploy/ │ │ │ │ ├── aliyun_cas_deploy.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── aliyun-cdn/ │ │ │ │ ├── aliyun_cdn.go │ │ │ │ ├── aliyun_cdn_test.go │ │ │ │ ├── consts.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── aliyun-clb/ │ │ │ │ ├── aliyun_clb.go │ │ │ │ ├── aliyun_clb_test.go │ │ │ │ ├── consts.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── aliyun-dcdn/ │ │ │ │ ├── aliyun_dcdn.go │ │ │ │ ├── aliyun_dcdn_test.go │ │ │ │ ├── consts.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── aliyun-ddospro/ │ │ │ │ ├── aliyun_ddospro.go │ │ │ │ ├── aliyun_ddospro_test.go │ │ │ │ ├── consts.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── aliyun-esa/ │ │ │ │ ├── aliyun_esa.go │ │ │ │ ├── aliyun_esa_test.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── aliyun-esa-saas/ │ │ │ │ ├── aliyun_esasaas.go │ │ │ │ ├── aliyun_esasaas_test.go │ │ │ │ ├── consts.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── aliyun-fc/ │ │ │ │ ├── aliyun_fc.go │ │ │ │ ├── aliyun_fc_test.go │ │ │ │ ├── consts.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── aliyun-ga/ │ │ │ │ ├── aliyun_ga.go │ │ │ │ ├── aliyun_ga_test.go │ │ │ │ ├── consts.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── aliyun-live/ │ │ │ │ ├── aliyun_live.go │ │ │ │ ├── aliyun_live_test.go │ │ │ │ ├── consts.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── aliyun-nlb/ │ │ │ │ ├── aliyun_nlb.go │ │ │ │ ├── aliyun_nlb_test.go │ │ │ │ ├── consts.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── aliyun-oss/ │ │ │ │ ├── aliyun_oss.go │ │ │ │ └── aliyun_oss_test.go │ │ │ ├── aliyun-vod/ │ │ │ │ ├── aliyun_vod.go │ │ │ │ ├── aliyun_vod_test.go │ │ │ │ ├── consts.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── aliyun-waf/ │ │ │ │ ├── aliyun_waf.go │ │ │ │ ├── aliyun_waf_test.go │ │ │ │ ├── consts.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── apisix/ │ │ │ │ ├── apisix.go │ │ │ │ ├── apisix_test.go │ │ │ │ └── consts.go │ │ │ ├── aws-acm/ │ │ │ │ └── aws_acm.go │ │ │ ├── aws-cloudfront/ │ │ │ │ ├── aws_cloudfront.go │ │ │ │ ├── aws_cloudfront_test.go │ │ │ │ └── consts.go │ │ │ ├── aws-iam/ │ │ │ │ └── aws_iam.go │ │ │ ├── azure-keyvault/ │ │ │ │ └── azure_keyvault.go │ │ │ ├── baiducloud-appblb/ │ │ │ │ ├── baiducloud_appblb.go │ │ │ │ ├── baiducloud_appblb_test.go │ │ │ │ └── consts.go │ │ │ ├── baiducloud-blb/ │ │ │ │ ├── baiducloud_blb.go │ │ │ │ ├── baiducloud_blb_test.go │ │ │ │ └── consts.go │ │ │ ├── baiducloud-cdn/ │ │ │ │ ├── baiducloud_cdn.go │ │ │ │ ├── baiducloud_cdn_test.go │ │ │ │ └── consts.go │ │ │ ├── baiducloud-cert/ │ │ │ │ └── baiducloud_cert.go │ │ │ ├── baishan-cdn/ │ │ │ │ ├── baishan_cdn.go │ │ │ │ ├── baishan_cdn_test.go │ │ │ │ └── consts.go │ │ │ ├── baotapanel/ │ │ │ │ ├── baotapanel.go │ │ │ │ └── baotapanel_test.go │ │ │ ├── baotapanel-console/ │ │ │ │ ├── baotapanel_console.go │ │ │ │ └── baotapanel_console_test.go │ │ │ ├── baotapanelgo/ │ │ │ │ ├── baotapanelgo.go │ │ │ │ └── baotapanelgo_test.go │ │ │ ├── baotapanelgo-console/ │ │ │ │ ├── baotapanelgo_console.go │ │ │ │ └── baotapanelgo_console_test.go │ │ │ ├── baotawaf/ │ │ │ │ ├── baotawaf.go │ │ │ │ └── baotawaf_test.go │ │ │ ├── baotawaf-console/ │ │ │ │ ├── baotawaf_console.go │ │ │ │ └── baotawaf_console_test.go │ │ │ ├── bunny-cdn/ │ │ │ │ ├── bunny_cdn.go │ │ │ │ └── bunny_cdn_test.go │ │ │ ├── byteplus-cdn/ │ │ │ │ ├── byteplus_cdn.go │ │ │ │ ├── byteplus_cdn_test.go │ │ │ │ └── consts.go │ │ │ ├── cachefly/ │ │ │ │ ├── cachefly.go │ │ │ │ └── cachefly_test.go │ │ │ ├── cdnfly/ │ │ │ │ ├── cdnfly.go │ │ │ │ ├── cdnfly_test.go │ │ │ │ └── consts.go │ │ │ ├── cpanel/ │ │ │ │ ├── consts.go │ │ │ │ ├── cpanel.go │ │ │ │ └── cpanel_test.go │ │ │ ├── ctcccloud-ao/ │ │ │ │ ├── consts.go │ │ │ │ ├── ctcccloud_ao.go │ │ │ │ └── ctcccloud_ao_test.go │ │ │ ├── ctcccloud-cdn/ │ │ │ │ ├── consts.go │ │ │ │ ├── ctcccloud_cdn.go │ │ │ │ └── ctcccloud_cdn_test.go │ │ │ ├── ctcccloud-cms/ │ │ │ │ ├── ctcccloud_cms.go │ │ │ │ └── ctcccloud_cms_test.go │ │ │ ├── ctcccloud-elb/ │ │ │ │ ├── consts.go │ │ │ │ ├── ctcccloud_elb.go │ │ │ │ └── ctcccloud_elb_test.go │ │ │ ├── ctcccloud-faas/ │ │ │ │ ├── ctcccloud_faas.go │ │ │ │ └── ctcccloud_faas_test.go │ │ │ ├── ctcccloud-icdn/ │ │ │ │ ├── consts.go │ │ │ │ ├── ctcccloud_icdn.go │ │ │ │ └── ctcccloud_icdn_test.go │ │ │ ├── ctcccloud-lvdn/ │ │ │ │ ├── consts.go │ │ │ │ ├── ctcccloud_lvdn.go │ │ │ │ └── ctcccloud_lvdn_test.go │ │ │ ├── dogecloud-cdn/ │ │ │ │ ├── consts.go │ │ │ │ ├── dogecloud_cdn.go │ │ │ │ └── dogecloud_cdn_test.go │ │ │ ├── dokploy/ │ │ │ │ ├── dokploy.go │ │ │ │ └── dokploy_test.go │ │ │ ├── flexcdn/ │ │ │ │ ├── consts.go │ │ │ │ ├── flexcdn.go │ │ │ │ └── flexcdn_test.go │ │ │ ├── flyio/ │ │ │ │ ├── flyio.go │ │ │ │ └── flyio_test.go │ │ │ ├── gcore-cdn/ │ │ │ │ ├── gcore_cdn.go │ │ │ │ └── gcore_cdn_test.go │ │ │ ├── goedge/ │ │ │ │ ├── consts.go │ │ │ │ ├── goedge.go │ │ │ │ └── goedge_test.go │ │ │ ├── huaweicloud-cdn/ │ │ │ │ ├── consts.go │ │ │ │ ├── huaweicloud_cdn.go │ │ │ │ ├── huaweicloud_cdn_test.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── huaweicloud-elb/ │ │ │ │ ├── consts.go │ │ │ │ ├── huaweicloud_elb.go │ │ │ │ ├── huaweicloud_elb_test.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── huaweicloud-obs/ │ │ │ │ ├── huaweicloud_obs.go │ │ │ │ └── huaweicloud_obs_test.go │ │ │ ├── huaweicloud-scm/ │ │ │ │ └── huaweicloud_scm.go │ │ │ ├── huaweicloud-waf/ │ │ │ │ ├── consts.go │ │ │ │ ├── huaweicloud_waf.go │ │ │ │ ├── huaweicloud_waf_test.go │ │ │ │ └── internal/ │ │ │ │ └── client.go │ │ │ ├── jdcloud-alb/ │ │ │ │ ├── consts.go │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ ├── jdcloud_alb.go │ │ │ │ └── jdcloud_alb_test.go │ │ │ ├── jdcloud-cdn/ │ │ │ │ ├── consts.go │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ ├── jdcloud_cdn.go │ │ │ │ └── jdcloud_cdn_test.go │ │ │ ├── jdcloud-live/ │ │ │ │ ├── consts.go │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ ├── jdcloud_live.go │ │ │ │ └── jdcloud_live_test.go │ │ │ ├── jdcloud-vod/ │ │ │ │ ├── consts.go │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ ├── jdcloud_vod.go │ │ │ │ └── jdcloud_vod_test.go │ │ │ ├── k8s-secret/ │ │ │ │ ├── k8s_secret.go │ │ │ │ └── k8s_secret_test.go │ │ │ ├── kong/ │ │ │ │ ├── consts.go │ │ │ │ ├── kong.go │ │ │ │ └── kong_test.go │ │ │ ├── ksyun-cdn/ │ │ │ │ ├── consts.go │ │ │ │ ├── ksyun_cdn.go │ │ │ │ └── ksyun_cdn_test.go │ │ │ ├── lecdn/ │ │ │ │ ├── consts.go │ │ │ │ ├── lecdn.go │ │ │ │ └── lecdn_test.go │ │ │ ├── local/ │ │ │ │ ├── consts.go │ │ │ │ ├── local.go │ │ │ │ └── local_test.go │ │ │ ├── mohua-mvh/ │ │ │ │ ├── mohua_mvh.go │ │ │ │ └── mohua_mvh_test.go │ │ │ ├── netlify/ │ │ │ │ ├── consts.go │ │ │ │ ├── netlify.go │ │ │ │ └── netlify_test.go │ │ │ ├── nginxproxymanager/ │ │ │ │ ├── consts.go │ │ │ │ ├── nginxproxymanager.go │ │ │ │ └── nginxproxymanager_test.go │ │ │ ├── proxmoxve/ │ │ │ │ ├── proxmoxve.go │ │ │ │ └── proxmoxve_test.go │ │ │ ├── qiniu-cdn/ │ │ │ │ ├── consts.go │ │ │ │ ├── qiniu_cdn.go │ │ │ │ └── qiniu_cdn_test.go │ │ │ ├── qiniu-kodo/ │ │ │ │ ├── qiniu_kodo.go │ │ │ │ └── qiniu_kodo_test.go │ │ │ ├── qiniu-pili/ │ │ │ │ ├── consts.go │ │ │ │ ├── qiniu_pili.go │ │ │ │ └── qiniu_pili_test.go │ │ │ ├── rainyun-rcdn/ │ │ │ │ ├── consts.go │ │ │ │ ├── rainyun_rcdn.go │ │ │ │ └── rainyun_rcdn_test.go │ │ │ ├── rainyun-sslcenter/ │ │ │ │ ├── rainyun_sslcenter.go │ │ │ │ └── rainyun_sslcenter_test.go │ │ │ ├── ratpanel/ │ │ │ │ ├── consts.go │ │ │ │ ├── ratpanel.go │ │ │ │ └── ratpanel_test.go │ │ │ ├── ratpanel-console/ │ │ │ │ ├── ratpanel_console.go │ │ │ │ └── ratpanel_console_test.go │ │ │ ├── s3/ │ │ │ │ ├── consts.go │ │ │ │ └── s3.go │ │ │ ├── safeline/ │ │ │ │ ├── consts.go │ │ │ │ ├── safeline.go │ │ │ │ └── safeline_test.go │ │ │ ├── ssh/ │ │ │ │ ├── consts.go │ │ │ │ ├── ssh.go │ │ │ │ └── ssh_test.go │ │ │ ├── synologydsm/ │ │ │ │ ├── synologydsm.go │ │ │ │ └── synologydsm_test.go │ │ │ ├── tencentcloud-cdn/ │ │ │ │ ├── consts.go │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ ├── tencentcloud_cdn.go │ │ │ │ └── tencentcloud_cdn_test.go │ │ │ ├── tencentcloud-clb/ │ │ │ │ ├── consts.go │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ ├── tencentcloud_clb.go │ │ │ │ └── tencentcloud_clb_test.go │ │ │ ├── tencentcloud-cos/ │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ ├── tencentcloud_cos.go │ │ │ │ └── tencentcloud_cos_test.go │ │ │ ├── tencentcloud-css/ │ │ │ │ ├── consts.go │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ ├── tencentcloud_css.go │ │ │ │ └── tencentcloud_css_test.go │ │ │ ├── tencentcloud-ecdn/ │ │ │ │ ├── consts.go │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ ├── tencentcloud_ecdn.go │ │ │ │ └── tencentcloud_ecdn_test.go │ │ │ ├── tencentcloud-eo/ │ │ │ │ ├── consts.go │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ ├── tencentcloud_eo.go │ │ │ │ └── tencentcloud_eo_test.go │ │ │ ├── tencentcloud-gaap/ │ │ │ │ ├── consts.go │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ ├── tencentcloud_gaap.go │ │ │ │ └── tencentcloud_gaap_test.go │ │ │ ├── tencentcloud-scf/ │ │ │ │ ├── consts.go │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ ├── tencentcloud_scf.go │ │ │ │ └── tencentcloud_scf_test.go │ │ │ ├── tencentcloud-ssl/ │ │ │ │ └── tencentcloud_ssl.go │ │ │ ├── tencentcloud-ssl-deploy/ │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ └── tencentcloud_ssl_deploy.go │ │ │ ├── tencentcloud-ssl-update/ │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ └── tencentcloud_ssl_update.go │ │ │ ├── tencentcloud-vod/ │ │ │ │ ├── consts.go │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ ├── tencentcloud_vod.go │ │ │ │ └── tencentcloud_vod_test.go │ │ │ ├── tencentcloud-waf/ │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ ├── tencentcloud_waf.go │ │ │ │ └── tencentcloud_waf_test.go │ │ │ ├── ucloud-ualb/ │ │ │ │ ├── consts.go │ │ │ │ ├── ucloud_ualb.go │ │ │ │ └── ucloud_ualb_test.go │ │ │ ├── ucloud-ucdn/ │ │ │ │ ├── ucloud_ucdn.go │ │ │ │ └── ucloud_ucdn_test.go │ │ │ ├── ucloud-uclb/ │ │ │ │ ├── consts.go │ │ │ │ ├── ucloud_uclb.go │ │ │ │ └── ucloud_uclb_test.go │ │ │ ├── ucloud-uewaf/ │ │ │ │ ├── ucloud_uewaf.go │ │ │ │ └── ucloud_uewaf_test.go │ │ │ ├── ucloud-upathx/ │ │ │ │ ├── ucloud_upathx.go │ │ │ │ └── ucloud_upathx_test.go │ │ │ ├── ucloud-us3/ │ │ │ │ ├── ucloud_us3.go │ │ │ │ └── ucloud_us3_test.go │ │ │ ├── unicloud-webhost/ │ │ │ │ ├── unicloud_webhost.go │ │ │ │ └── unicloud_webhost_test.go │ │ │ ├── upyun-cdn/ │ │ │ │ ├── consts.go │ │ │ │ ├── upyun_cdn.go │ │ │ │ └── upyun_cdn_test.go │ │ │ ├── upyun-file/ │ │ │ │ ├── upyun_file.go │ │ │ │ └── upyun_file_test.go │ │ │ ├── volcengine-alb/ │ │ │ │ ├── consts.go │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ ├── volcengine_alb.go │ │ │ │ └── volcengine_alb_test.go │ │ │ ├── volcengine-cdn/ │ │ │ │ ├── consts.go │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ ├── volcengine_cdn.go │ │ │ │ └── volcengine_cdn_test.go │ │ │ ├── volcengine-certcenter/ │ │ │ │ └── volcengine_certcenter.go │ │ │ ├── volcengine-clb/ │ │ │ │ ├── consts.go │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ ├── volcengine_clb.go │ │ │ │ └── volcengine_clb_test.go │ │ │ ├── volcengine-dcdn/ │ │ │ │ ├── consts.go │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ ├── volcengine_dcdn.go │ │ │ │ └── volcengine_dcdn_test.go │ │ │ ├── volcengine-imagex/ │ │ │ │ ├── volcengine_imagex.go │ │ │ │ └── volcengine_imagex_test.go │ │ │ ├── volcengine-live/ │ │ │ │ ├── consts.go │ │ │ │ ├── volcengine_live.go │ │ │ │ └── volcengine_live_test.go │ │ │ ├── volcengine-tos/ │ │ │ │ ├── volcengine_tos.go │ │ │ │ └── volcengine_tos_test.go │ │ │ ├── volcengine-vod/ │ │ │ │ ├── consts.go │ │ │ │ ├── volcengine_vod.go │ │ │ │ └── volcengine_vod_test.go │ │ │ ├── volcengine-waf/ │ │ │ │ ├── consts.go │ │ │ │ ├── internal/ │ │ │ │ │ └── client.go │ │ │ │ ├── volcengine_waf.go │ │ │ │ └── volcengine_waf_test.go │ │ │ ├── wangsu-cdn/ │ │ │ │ ├── consts.go │ │ │ │ ├── wangsu_cdn.go │ │ │ │ └── wangsu_cdn_test.go │ │ │ ├── wangsu-cdnpro/ │ │ │ │ ├── consts.go │ │ │ │ ├── wangsu_cdnpro.go │ │ │ │ └── wangsu_cdnpro_test.go │ │ │ ├── wangsu-certificate/ │ │ │ │ ├── wangsu_certificate.go │ │ │ │ └── wangsu_certificate_test.go │ │ │ └── webhook/ │ │ │ ├── webhook.go │ │ │ └── webhook_test.go │ │ ├── notifier/ │ │ │ ├── provider.go │ │ │ └── providers/ │ │ │ ├── dingtalkbot/ │ │ │ │ ├── dingtalkbot.go │ │ │ │ └── dingtalkbot_test.go │ │ │ ├── discordbot/ │ │ │ │ ├── discordbot.go │ │ │ │ └── discordbot_test.go │ │ │ ├── email/ │ │ │ │ ├── consts.go │ │ │ │ ├── email.go │ │ │ │ └── email_test.go │ │ │ ├── larkbot/ │ │ │ │ ├── larkbot.go │ │ │ │ └── larkbot_test.go │ │ │ ├── mattermost/ │ │ │ │ ├── mattermost.go │ │ │ │ └── mattermost_test.go │ │ │ ├── slackbot/ │ │ │ │ ├── slackbot.go │ │ │ │ └── slackbot_test.go │ │ │ ├── telegrambot/ │ │ │ │ ├── telegrambot.go │ │ │ │ └── telegrambot_test.go │ │ │ ├── webhook/ │ │ │ │ ├── webhook.go │ │ │ │ └── webhook_test.go │ │ │ └── wecombot/ │ │ │ ├── wecombot.go │ │ │ └── wecombot_test.go │ │ └── shared.go │ ├── forks/ │ │ └── gitlab.ecloud.com/ │ │ └── ecloud/ │ │ ├── README.md │ │ ├── ecloudsdkclouddns@v1.0.1/ │ │ │ ├── client.go │ │ │ ├── go.mod │ │ │ └── model/ │ │ │ ├── create_record_body.go │ │ │ ├── create_record_openapi_body.go │ │ │ ├── create_record_openapi_request.go │ │ │ ├── create_record_openapi_response.go │ │ │ ├── create_record_openapi_response_body.go │ │ │ ├── create_record_openapi_response_tags.go │ │ │ ├── create_record_request.go │ │ │ ├── create_record_response.go │ │ │ ├── create_record_response_body.go │ │ │ ├── create_record_response_tags.go │ │ │ ├── delete_record_body.go │ │ │ ├── delete_record_openapi_body.go │ │ │ ├── delete_record_openapi_request.go │ │ │ ├── delete_record_openapi_response.go │ │ │ ├── delete_record_openapi_response_body.go │ │ │ ├── delete_record_request.go │ │ │ ├── delete_record_response.go │ │ │ ├── delete_record_response_body.go │ │ │ ├── list_record_body.go │ │ │ ├── list_record_openapi_body.go │ │ │ ├── list_record_openapi_query.go │ │ │ ├── list_record_openapi_request.go │ │ │ ├── list_record_openapi_response.go │ │ │ ├── list_record_openapi_response_body.go │ │ │ ├── list_record_openapi_response_data.go │ │ │ ├── list_record_openapi_response_tags.go │ │ │ ├── list_record_query.go │ │ │ ├── list_record_request.go │ │ │ ├── list_record_response.go │ │ │ ├── list_record_response_body.go │ │ │ ├── list_record_response_results.go │ │ │ ├── modify_record_body.go │ │ │ ├── modify_record_openapi_body.go │ │ │ ├── modify_record_openapi_request.go │ │ │ ├── modify_record_openapi_response.go │ │ │ ├── modify_record_openapi_response_body.go │ │ │ ├── modify_record_openapi_response_tags.go │ │ │ ├── modify_record_request.go │ │ │ ├── modify_record_response.go │ │ │ └── modify_record_response_body.go │ │ └── ecloudsdkcore@v1.0.0/ │ │ ├── api_client.go │ │ ├── api_response.go │ │ ├── config/ │ │ │ └── config.go │ │ ├── configuration.go │ │ ├── go.mod │ │ ├── http_request.go │ │ ├── open_api_request.go │ │ └── position/ │ │ └── http_position.go │ ├── logging/ │ │ ├── handler.go │ │ └── record.go │ ├── sdk3rd/ │ │ ├── 1panel/ │ │ │ ├── api_settings_ssl_update.go │ │ │ ├── api_website_get.go │ │ │ ├── api_website_https_get.go │ │ │ ├── api_website_https_post.go │ │ │ ├── api_website_search.go │ │ │ ├── api_website_ssl_get.go │ │ │ ├── api_website_ssl_search.go │ │ │ ├── api_website_ssl_upload.go │ │ │ ├── client.go │ │ │ ├── types.go │ │ │ └── v2/ │ │ │ ├── api_core_settings_ssl_update.go │ │ │ ├── api_website_get.go │ │ │ ├── api_website_https_get.go │ │ │ ├── api_website_https_post.go │ │ │ ├── api_website_search.go │ │ │ ├── api_website_ssl_get.go │ │ │ ├── api_website_ssl_search.go │ │ │ ├── api_website_ssl_upload.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── 51dnscom/ │ │ │ ├── api_domain_list.go │ │ │ ├── api_record_create.go │ │ │ ├── api_record_remove.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── apisix/ │ │ │ ├── api_ssl_update.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── azure/ │ │ │ └── env/ │ │ │ └── config.go │ │ ├── baiducloud/ │ │ │ └── cert/ │ │ │ ├── cert.go │ │ │ ├── client.go │ │ │ └── model.go │ │ ├── baishan/ │ │ │ ├── api_get_domain_config.go │ │ │ ├── api_get_domain_list.go │ │ │ ├── api_set_domain_config.go │ │ │ ├── api_upload_domain_certificate.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── btpanel/ │ │ │ ├── api_config_save_panel_ssl.go │ │ │ ├── api_mod_proxy_com_set_ssl.go │ │ │ ├── api_site_set_ssl.go │ │ │ ├── api_ssl_cert_save_cert.go │ │ │ ├── api_ssl_set_batch_cert_to_site.go │ │ │ ├── api_system_service_admin.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── btpanelgo/ │ │ │ ├── api_config_set_panel_ssl.go │ │ │ ├── api_datalist_get_data_list.go │ │ │ ├── api_files_upload.go │ │ │ ├── api_panel_get_config.go │ │ │ ├── api_site_get_project_list.go │ │ │ ├── api_site_set_site_pfx_ssl.go │ │ │ ├── api_site_set_site_ssl.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── btwaf/ │ │ │ ├── api_config_set_cert.go │ │ │ ├── api_get_site_list.go │ │ │ ├── api_modify_site.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── bunny/ │ │ │ ├── api_add_custom_certificate.go │ │ │ └── client.go │ │ ├── cachefly/ │ │ │ ├── api_create_certificate.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── cdnfly/ │ │ │ ├── api_create_cert.go │ │ │ ├── api_get_site.go │ │ │ ├── api_update_cert.go │ │ │ ├── api_update_site.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── cpanel/ │ │ │ ├── api_ssl_install_ssl.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── ctyun/ │ │ │ ├── ao/ │ │ │ │ ├── api_create_cert.go │ │ │ │ ├── api_get_domain_config.go │ │ │ │ ├── api_list_certs.go │ │ │ │ ├── api_modify_domain_config.go │ │ │ │ ├── api_query_cert.go │ │ │ │ ├── api_query_domains.go │ │ │ │ ├── client.go │ │ │ │ └── types.go │ │ │ ├── cdn/ │ │ │ │ ├── api_create_cert.go │ │ │ │ ├── api_query_cert_detail.go │ │ │ │ ├── api_query_cert_list.go │ │ │ │ ├── api_query_domain_detail.go │ │ │ │ ├── api_query_domain_list.go │ │ │ │ ├── api_update_domain.go │ │ │ │ ├── client.go │ │ │ │ └── types.go │ │ │ ├── cms/ │ │ │ │ ├── api_get_certificate_list.go │ │ │ │ ├── api_upload_certificate.go │ │ │ │ ├── client.go │ │ │ │ └── types.go │ │ │ ├── dns/ │ │ │ │ ├── api_add_record.go │ │ │ │ ├── api_delete_record.go │ │ │ │ ├── api_query_record_list.go │ │ │ │ ├── api_update_record.go │ │ │ │ ├── client.go │ │ │ │ └── types.go │ │ │ ├── elb/ │ │ │ │ ├── api_create_certificate.go │ │ │ │ ├── api_list_certificates.go │ │ │ │ ├── api_list_listeners.go │ │ │ │ ├── api_show_listener.go │ │ │ │ ├── api_update_listener.go │ │ │ │ ├── client.go │ │ │ │ └── types.go │ │ │ ├── faas/ │ │ │ │ ├── api_get_custom_domain.go │ │ │ │ ├── api_update_custom_domain.go │ │ │ │ ├── client.go │ │ │ │ └── types.go │ │ │ ├── icdn/ │ │ │ │ ├── api_create_cert.go │ │ │ │ ├── api_query_cert_detail.go │ │ │ │ ├── api_query_cert_list.go │ │ │ │ ├── api_query_domain_detail.go │ │ │ │ ├── api_query_domain_list.go │ │ │ │ ├── api_update_domain.go │ │ │ │ ├── client.go │ │ │ │ └── types.go │ │ │ ├── lvdn/ │ │ │ │ ├── api_create_cert.go │ │ │ │ ├── api_query_cert_detail.go │ │ │ │ ├── api_query_cert_list.go │ │ │ │ ├── api_query_domain_detail.go │ │ │ │ ├── api_query_domain_list.go │ │ │ │ ├── api_update_domain.go │ │ │ │ ├── client.go │ │ │ │ └── types.go │ │ │ └── openapi/ │ │ │ └── client.go │ │ ├── dcloud/ │ │ │ └── unicloud/ │ │ │ ├── api_create_domain_with_cert.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── dnsla/ │ │ │ ├── api_create_record.go │ │ │ ├── api_delete_record.go │ │ │ ├── api_list_domains.go │ │ │ ├── api_list_records.go │ │ │ ├── api_update_record.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── dogecloud/ │ │ │ ├── api_bind_cdn_cert.go │ │ │ ├── api_list_cdn_domain.go │ │ │ ├── api_upload_cdn_cert.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── dokploy/ │ │ │ ├── api_certificates_all.go │ │ │ ├── api_certificates_create.go │ │ │ ├── api_user_get.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── dynv6/ │ │ │ ├── api_add_record.go │ │ │ ├── api_delete_record.go │ │ │ ├── api_list_records.go │ │ │ ├── api_list_zones.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── flexcdn/ │ │ │ ├── api_update_ssl_cert.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── flyio/ │ │ │ ├── api_import_custom_certificate.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── gcore/ │ │ │ ├── endpoint.go │ │ │ └── signer.go │ │ ├── gname/ │ │ │ ├── api_add_domain_resolution.go │ │ │ ├── api_delete_domain_resolution.go │ │ │ ├── api_list_domain_resolution.go │ │ │ ├── api_modify_domain_resolution.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── goedge/ │ │ │ ├── api_update_ssl_cert.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── lecdn/ │ │ │ └── v3/ │ │ │ ├── client/ │ │ │ │ ├── api_update_certificate.go │ │ │ │ ├── client.go │ │ │ │ └── types.go │ │ │ └── master/ │ │ │ ├── api_update_certificate.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── netlify/ │ │ │ ├── api_provision_site_tls_certificate.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── nginxproxymanager/ │ │ │ ├── api_nginx_create_certificate.go │ │ │ ├── api_nginx_list_certificates.go │ │ │ ├── api_nginx_list_dead_hosts.go │ │ │ ├── api_nginx_list_proxy_hosts.go │ │ │ ├── api_nginx_list_redirection_hosts.go │ │ │ ├── api_nginx_list_streams.go │ │ │ ├── api_nginx_update_dead_host.go │ │ │ ├── api_nginx_update_proxy_host.go │ │ │ ├── api_nginx_update_redirection_host.go │ │ │ ├── api_nginx_update_stream.go │ │ │ ├── api_nginx_upload_certificate.go │ │ │ ├── api_settings_get_default_site.go │ │ │ ├── api_settings_set_default_site.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── qingcloud/ │ │ │ └── dns/ │ │ │ ├── api_create_record.go │ │ │ ├── api_delete_record.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── qiniu/ │ │ │ ├── auth.go │ │ │ ├── cdn.go │ │ │ ├── kodo.go │ │ │ ├── sslcert.go │ │ │ └── util.go │ │ ├── rainyun/ │ │ │ ├── api_rcdn_instance_ssl_bind.go │ │ │ ├── api_ssl_center_create.go │ │ │ ├── api_ssl_center_get.go │ │ │ ├── api_ssl_center_list.go │ │ │ ├── api_ssl_center_update.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── ratpanel/ │ │ │ ├── api_set_cert_update.go │ │ │ ├── api_set_setting_cert.go │ │ │ ├── api_set_website_cert.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── safeline/ │ │ │ ├── api_update_certificate.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── synologydsm/ │ │ │ ├── api_auth_login.go │ │ │ ├── api_auth_logout.go │ │ │ ├── api_core_certificate_crt_list.go │ │ │ ├── api_core_certificate_import.go │ │ │ ├── api_core_certificate_service_set.go │ │ │ ├── api_info_query.go │ │ │ ├── client.go │ │ │ ├── types.go │ │ │ └── utils.go │ │ ├── ucloud/ │ │ │ ├── ucdn/ │ │ │ │ ├── api_get_ucdn_domain_config.go │ │ │ │ ├── api_update_ucdn_domain_https_config_v2.go │ │ │ │ ├── client.go │ │ │ │ └── types.go │ │ │ ├── udnr/ │ │ │ │ ├── api_add_domain_dns.go │ │ │ │ ├── api_delete_domain_dns.go │ │ │ │ ├── api_query_domain_dns.go │ │ │ │ ├── client.go │ │ │ │ └── types.go │ │ │ ├── uewaf/ │ │ │ │ ├── api_add_waf_domain_certificate_info.go │ │ │ │ └── client.go │ │ │ ├── ufile/ │ │ │ │ ├── api_add_ufile_ssl_cert.go │ │ │ │ └── client.go │ │ │ ├── ulb/ │ │ │ │ ├── api_add_ssl_binding.go │ │ │ │ ├── api_bind_ssl.go │ │ │ │ ├── api_create_ssl.go │ │ │ │ ├── api_delete_ssl_binding.go │ │ │ │ ├── api_describe_listeners.go │ │ │ │ ├── api_describe_ssl.go │ │ │ │ ├── api_describe_ssl_v2.go │ │ │ │ ├── api_describe_vserver.go │ │ │ │ ├── api_unbind_ssl.go │ │ │ │ ├── api_update_listener_attribute.go │ │ │ │ └── client.go │ │ │ ├── upathx/ │ │ │ │ ├── api_bind_pathx_ssl.go │ │ │ │ ├── api_create_pathx_ssl.go │ │ │ │ ├── api_describe_pathx_ssl.go │ │ │ │ ├── api_unbind_pathx_ssl.go │ │ │ │ └── client.go │ │ │ └── ussl/ │ │ │ ├── api_download_certificate.go │ │ │ ├── api_get_certificate_detail_info.go │ │ │ ├── api_get_certificate_list.go │ │ │ ├── api_upload_normal_certificate.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── upyun/ │ │ │ └── console/ │ │ │ ├── api_get_buckets.go │ │ │ ├── api_get_https_certificate_manager.go │ │ │ ├── api_get_https_service_manager.go │ │ │ ├── api_migrate_https_domain.go │ │ │ ├── api_update_https_certificate_manager.go │ │ │ ├── api_upload_https_certificate.go │ │ │ ├── client.go │ │ │ └── types.go │ │ ├── wangsu/ │ │ │ ├── cdn/ │ │ │ │ ├── api_batch_update_certificate_config.go │ │ │ │ ├── client.go │ │ │ │ └── types.go │ │ │ ├── cdnpro/ │ │ │ │ ├── api_create_certificate.go │ │ │ │ ├── api_create_deployment_task.go │ │ │ │ ├── api_get_deployment_task_detail.go │ │ │ │ ├── api_get_hostname_detail.go │ │ │ │ ├── api_update_certificate.go │ │ │ │ ├── client.go │ │ │ │ └── types.go │ │ │ ├── certificate/ │ │ │ │ ├── api_create_certificate.go │ │ │ │ ├── api_list_certificates.go │ │ │ │ ├── api_update_certificate.go │ │ │ │ ├── client.go │ │ │ │ └── types.go │ │ │ └── openapi/ │ │ │ └── client.go │ │ └── xinnet/ │ │ ├── api_dns_create.go │ │ ├── api_dns_delete.go │ │ ├── client.go │ │ └── types.go │ └── utils/ │ ├── cert/ │ │ ├── common.go │ │ ├── comparer.go │ │ ├── converter.go │ │ ├── extractor.go │ │ ├── hostname/ │ │ │ ├── hostname.go │ │ │ └── hostname_test.go │ │ ├── key/ │ │ │ └── key.go │ │ ├── parser.go │ │ ├── transformer.go │ │ └── x509/ │ │ └── x509.go │ ├── crypto/ │ │ └── aes.go │ ├── env/ │ │ └── get.go │ ├── file/ │ │ └── io.go │ ├── filepath/ │ │ └── path.go │ ├── http/ │ │ ├── parser.go │ │ └── transport.go │ ├── maps/ │ │ ├── get.go │ │ └── marshal.go │ ├── ssh/ │ │ ├── cmd.go │ │ └── io.go │ ├── tls/ │ │ └── config.go │ └── wait/ │ ├── delay.go │ └── until.go └── ui/ ├── .gitignore ├── embed.go ├── eslint.config.mjs ├── index.html ├── package.json ├── prettier.config.mjs ├── public/ │ └── robots.txt ├── src/ │ ├── App.tsx │ ├── api/ │ │ ├── certificates.ts │ │ ├── notifications.ts │ │ ├── statistics.ts │ │ └── workflows.ts │ ├── components/ │ │ ├── AppDocument.tsx │ │ ├── AppLocale.tsx │ │ ├── AppTheme.tsx │ │ ├── AppVersion.tsx │ │ ├── CodeTextInput.tsx │ │ ├── CopyableText.tsx │ │ ├── DrawerForm.tsx │ │ ├── Empty.tsx │ │ ├── FileTextInput.tsx │ │ ├── ModalForm.tsx │ │ ├── MultipleInput.tsx │ │ ├── MultipleSplitValueInput.tsx │ │ ├── Show.tsx │ │ ├── Tips.tsx │ │ ├── access/ │ │ │ ├── AccessEditDrawer.tsx │ │ │ ├── AccessForm.tsx │ │ │ ├── AccessSelect.tsx │ │ │ └── forms/ │ │ │ ├── AccessConfigFieldsProvider.tsx │ │ │ ├── AccessConfigFieldsProvider1Panel.tsx │ │ │ ├── AccessConfigFieldsProvider35cn.tsx │ │ │ ├── AccessConfigFieldsProvider51DNScom.tsx │ │ │ ├── AccessConfigFieldsProviderACMECA.tsx │ │ │ ├── AccessConfigFieldsProviderACMEDNS.tsx │ │ │ ├── AccessConfigFieldsProviderACMEHttpReq.tsx │ │ │ ├── AccessConfigFieldsProviderAPISIX.tsx │ │ │ ├── AccessConfigFieldsProviderAWS.tsx │ │ │ ├── AccessConfigFieldsProviderActalisSSL.tsx │ │ │ ├── AccessConfigFieldsProviderAkamai.tsx │ │ │ ├── AccessConfigFieldsProviderAliyun.tsx │ │ │ ├── AccessConfigFieldsProviderArvanCloud.tsx │ │ │ ├── AccessConfigFieldsProviderAzure.tsx │ │ │ ├── AccessConfigFieldsProviderBaiduCloud.tsx │ │ │ ├── AccessConfigFieldsProviderBaishan.tsx │ │ │ ├── AccessConfigFieldsProviderBaotaPanel.tsx │ │ │ ├── AccessConfigFieldsProviderBaotaPanelGo.tsx │ │ │ ├── AccessConfigFieldsProviderBaotaWAF.tsx │ │ │ ├── AccessConfigFieldsProviderBookMyName.tsx │ │ │ ├── AccessConfigFieldsProviderBunny.tsx │ │ │ ├── AccessConfigFieldsProviderBytePlus.tsx │ │ │ ├── AccessConfigFieldsProviderCMCCCloud.tsx │ │ │ ├── AccessConfigFieldsProviderCPanel.tsx │ │ │ ├── AccessConfigFieldsProviderCTCCCloud.tsx │ │ │ ├── AccessConfigFieldsProviderCacheFly.tsx │ │ │ ├── AccessConfigFieldsProviderCdnfly.tsx │ │ │ ├── AccessConfigFieldsProviderClouDNS.tsx │ │ │ ├── AccessConfigFieldsProviderCloudflare.tsx │ │ │ ├── AccessConfigFieldsProviderConstellix.tsx │ │ │ ├── AccessConfigFieldsProviderDNSExit.tsx │ │ │ ├── AccessConfigFieldsProviderDNSLA.tsx │ │ │ ├── AccessConfigFieldsProviderDNSMadeEasy.tsx │ │ │ ├── AccessConfigFieldsProviderDeSEC.tsx │ │ │ ├── AccessConfigFieldsProviderDigiCert.tsx │ │ │ ├── AccessConfigFieldsProviderDigitalOcean.tsx │ │ │ ├── AccessConfigFieldsProviderDingTalkBot.tsx │ │ │ ├── AccessConfigFieldsProviderDiscordBot.tsx │ │ │ ├── AccessConfigFieldsProviderDogeCloud.tsx │ │ │ ├── AccessConfigFieldsProviderDokploy.tsx │ │ │ ├── AccessConfigFieldsProviderDuckDNS.tsx │ │ │ ├── AccessConfigFieldsProviderDynu.tsx │ │ │ ├── AccessConfigFieldsProviderDynv6.tsx │ │ │ ├── AccessConfigFieldsProviderEmail.tsx │ │ │ ├── AccessConfigFieldsProviderFlexCDN.tsx │ │ │ ├── AccessConfigFieldsProviderFlyIO.tsx │ │ │ ├── AccessConfigFieldsProviderGandinet.tsx │ │ │ ├── AccessConfigFieldsProviderGcore.tsx │ │ │ ├── AccessConfigFieldsProviderGlobalSignAtlas.tsx │ │ │ ├── AccessConfigFieldsProviderGname.tsx │ │ │ ├── AccessConfigFieldsProviderGoDaddy.tsx │ │ │ ├── AccessConfigFieldsProviderGoEdge.tsx │ │ │ ├── AccessConfigFieldsProviderGoogleTrustServices.tsx │ │ │ ├── AccessConfigFieldsProviderHetzner.tsx │ │ │ ├── AccessConfigFieldsProviderHostingde.tsx │ │ │ ├── AccessConfigFieldsProviderHostinger.tsx │ │ │ ├── AccessConfigFieldsProviderHuaweiCloud.tsx │ │ │ ├── AccessConfigFieldsProviderIONOS.tsx │ │ │ ├── AccessConfigFieldsProviderInfomaniak.tsx │ │ │ ├── AccessConfigFieldsProviderJDCloud.tsx │ │ │ ├── AccessConfigFieldsProviderKong.tsx │ │ │ ├── AccessConfigFieldsProviderKsyun.tsx │ │ │ ├── AccessConfigFieldsProviderKubernetes.tsx │ │ │ ├── AccessConfigFieldsProviderLarkBot.tsx │ │ │ ├── AccessConfigFieldsProviderLeCDN.tsx │ │ │ ├── AccessConfigFieldsProviderLinode.tsx │ │ │ ├── AccessConfigFieldsProviderLiteSSL.tsx │ │ │ ├── AccessConfigFieldsProviderMattermost.tsx │ │ │ ├── AccessConfigFieldsProviderMohua.tsx │ │ │ ├── AccessConfigFieldsProviderNS1.tsx │ │ │ ├── AccessConfigFieldsProviderNameDotCom.tsx │ │ │ ├── AccessConfigFieldsProviderNameSilo.tsx │ │ │ ├── AccessConfigFieldsProviderNamecheap.tsx │ │ │ ├── AccessConfigFieldsProviderNetcup.tsx │ │ │ ├── AccessConfigFieldsProviderNetlify.tsx │ │ │ ├── AccessConfigFieldsProviderNginxProxyManager.tsx │ │ │ ├── AccessConfigFieldsProviderOVHcloud.tsx │ │ │ ├── AccessConfigFieldsProviderPorkbun.tsx │ │ │ ├── AccessConfigFieldsProviderPowerDNS.tsx │ │ │ ├── AccessConfigFieldsProviderProxmoxVE.tsx │ │ │ ├── AccessConfigFieldsProviderQingCloud.tsx │ │ │ ├── AccessConfigFieldsProviderQiniu.tsx │ │ │ ├── AccessConfigFieldsProviderRFC2136.tsx │ │ │ ├── AccessConfigFieldsProviderRainYun.tsx │ │ │ ├── AccessConfigFieldsProviderRatPanel.tsx │ │ │ ├── AccessConfigFieldsProviderS3.tsx │ │ │ ├── AccessConfigFieldsProviderSSH.tsx │ │ │ ├── AccessConfigFieldsProviderSSLCom.tsx │ │ │ ├── AccessConfigFieldsProviderSafeLine.tsx │ │ │ ├── AccessConfigFieldsProviderSectigo.tsx │ │ │ ├── AccessConfigFieldsProviderSlackBot.tsx │ │ │ ├── AccessConfigFieldsProviderSpaceship.tsx │ │ │ ├── AccessConfigFieldsProviderSynologyDSM.tsx │ │ │ ├── AccessConfigFieldsProviderTechnitiumDNS.tsx │ │ │ ├── AccessConfigFieldsProviderTelegramBot.tsx │ │ │ ├── AccessConfigFieldsProviderTencentCloud.tsx │ │ │ ├── AccessConfigFieldsProviderTodayNIC.tsx │ │ │ ├── AccessConfigFieldsProviderUCloud.tsx │ │ │ ├── AccessConfigFieldsProviderUniCloud.tsx │ │ │ ├── AccessConfigFieldsProviderUpyun.tsx │ │ │ ├── AccessConfigFieldsProviderVercel.tsx │ │ │ ├── AccessConfigFieldsProviderVolcEngine.tsx │ │ │ ├── AccessConfigFieldsProviderVultr.tsx │ │ │ ├── AccessConfigFieldsProviderWangsu.tsx │ │ │ ├── AccessConfigFieldsProviderWeComBot.tsx │ │ │ ├── AccessConfigFieldsProviderWebhook.tsx │ │ │ ├── AccessConfigFieldsProviderWestcn.tsx │ │ │ ├── AccessConfigFieldsProviderXinnet.tsx │ │ │ ├── AccessConfigFieldsProviderZeroSSL.tsx │ │ │ ├── _context.ts │ │ │ └── _hooks.ts │ │ ├── certificate/ │ │ │ ├── CertificateDetail.tsx │ │ │ └── CertificateDetailDrawer.tsx │ │ ├── icons/ │ │ │ ├── IconLanguageEnZh.tsx │ │ │ ├── IconLanguageZhEn.tsx │ │ │ ├── createIconComponent.ts │ │ │ └── index.ts │ │ ├── preset/ │ │ │ ├── PresetNotifyTemplatesPopselect.tsx │ │ │ └── PresetScriptTemplatesPopselect.tsx │ │ ├── provider/ │ │ │ ├── ACMEDns01ProviderSelect.tsx │ │ │ ├── ACMEHttp01ProviderSelect.tsx │ │ │ ├── AccessProviderPicker.tsx │ │ │ ├── AccessProviderSelect.tsx │ │ │ ├── CAProviderSelect.tsx │ │ │ ├── DeploymentProviderPicker.tsx │ │ │ ├── DeploymentProviderSelect.tsx │ │ │ ├── NotificationProviderPicker.tsx │ │ │ ├── NotificationProviderSelect.tsx │ │ │ └── _shared.ts │ │ └── workflow/ │ │ ├── WorkflowGraphExportBox.tsx │ │ ├── WorkflowGraphExportModal.tsx │ │ ├── WorkflowGraphImportInputBox.tsx │ │ ├── WorkflowGraphImportModal.tsx │ │ ├── WorkflowRunDetail.tsx │ │ ├── WorkflowRunDetailDrawer.tsx │ │ ├── WorkflowStatus.tsx │ │ └── designer/ │ │ ├── Designer.tsx │ │ ├── Minimap.tsx │ │ ├── NodeDrawer.tsx │ │ ├── NodeRender.tsx │ │ ├── NodeRenderContext.ts │ │ ├── Toolbar.tsx │ │ ├── _context.ts │ │ ├── _util.ts │ │ ├── elements/ │ │ │ ├── Adder.tsx │ │ │ ├── BranchAdder.tsx │ │ │ ├── Collapse.tsx │ │ │ ├── DragHighlightAdder.tsx │ │ │ ├── DragNode.tsx │ │ │ ├── DraggingAdder.tsx │ │ │ ├── Null.tsx │ │ │ ├── TryCatchCollapse.tsx │ │ │ └── index.ts │ │ ├── flowgram.css │ │ ├── forms/ │ │ │ ├── BizApplyNodeConfigDrawer.tsx │ │ │ ├── BizApplyNodeConfigFieldsProvider.tsx │ │ │ ├── BizApplyNodeConfigFieldsProviderAWSRoute53.tsx │ │ │ ├── BizApplyNodeConfigFieldsProviderAliyunESA.tsx │ │ │ ├── BizApplyNodeConfigFieldsProviderHuaweiCloudDNS.tsx │ │ │ ├── BizApplyNodeConfigFieldsProviderJDCloudDNS.tsx │ │ │ ├── BizApplyNodeConfigFieldsProviderLocal.tsx │ │ │ ├── BizApplyNodeConfigFieldsProviderS3.tsx │ │ │ ├── BizApplyNodeConfigFieldsProviderSSH.tsx │ │ │ ├── BizApplyNodeConfigForm.tsx │ │ │ ├── BizDeployNodeConfigDrawer.tsx │ │ │ ├── BizDeployNodeConfigFieldsProvider.tsx │ │ │ ├── BizDeployNodeConfigFieldsProvider1Panel.tsx │ │ │ ├── BizDeployNodeConfigFieldsProvider1PanelConsole.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderAPISIX.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderAWSACM.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderAWSCloudFront.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderAWSIAM.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderAliyunALB.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderAliyunAPIGW.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderAliyunCAS.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderAliyunCASDeploy.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderAliyunCDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderAliyunCLB.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderAliyunDCDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderAliyunDDoSPro.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderAliyunESA.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderAliyunESASaaS.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderAliyunFC.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderAliyunGA.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderAliyunLive.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderAliyunNLB.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderAliyunOSS.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderAliyunVOD.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderAliyunWAF.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderAzureKeyVault.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderBaiduCloudAppBLB.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderBaiduCloudBLB.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderBaiduCloudCDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderBaishanCDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderBaotaPanel.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderBaotaPanelConsole.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderBaotaPanelGo.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderBaotaPanelGoConsole.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderBaotaWAF.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderBaotaWAFConsole.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderBunnyCDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderBytePlusCDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderCPanel.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderCTCCCloudAO.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderCTCCCloudCDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderCTCCCloudELB.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderCTCCCloudFaaS.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderCTCCCloudICDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderCTCCCloudLVDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderCdnfly.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderDogeCloudCDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderFlexCDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderFlyIO.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderGcoreCDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderGoEdge.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderHuaweiCloudCDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderHuaweiCloudELB.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderHuaweiCloudOBS.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderHuaweiCloudWAF.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderJDCloudALB.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderJDCloudCDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderJDCloudLive.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderJDCloudVOD.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderKong.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderKsyunCDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderKubernetesSecret.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderLeCDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderLocal.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderMohuaMVH.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderNetlify.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderNginxProxyManager.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderProxmoxVE.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderQiniuCDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderQiniuKodo.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderQiniuPili.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderRainYunRCDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderRainYunSSLCenter.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderRatPanel.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderS3.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderSSH.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderSafeLine.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderSynologyDSM.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderTencentCloudCDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderTencentCloudCLB.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderTencentCloudCOS.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderTencentCloudCSS.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderTencentCloudECDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderTencentCloudEO.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderTencentCloudGAAP.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderTencentCloudSCF.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderTencentCloudSSL.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderTencentCloudSSLDeploy.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderTencentCloudSSLUpdate.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderTencentCloudVOD.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderTencentCloudWAF.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderUCloudUALB.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderUCloudUCDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderUCloudUCLB.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderUCloudUEWAF.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderUCloudUPathX.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderUCloudUS3.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderUniCloudWebHost.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderUpyunCDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderUpyunFile.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderVolcEngineALB.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderVolcEngineCDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderVolcEngineCLB.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderVolcEngineCertCenter.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderVolcEngineDCDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderVolcEngineImageX.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderVolcEngineLive.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderVolcEngineTOS.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderVolcEngineVOD.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderVolcEngineWAF.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderWangsuCDN.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderWangsuCDNPro.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderWangsuCertificate.tsx │ │ │ ├── BizDeployNodeConfigFieldsProviderWebhook.tsx │ │ │ ├── BizDeployNodeConfigForm.tsx │ │ │ ├── BizMonitorNodeConfigDrawer.tsx │ │ │ ├── BizMonitorNodeConfigForm.tsx │ │ │ ├── BizNotifyNodeConfigDrawer.tsx │ │ │ ├── BizNotifyNodeConfigFieldsProvider.tsx │ │ │ ├── BizNotifyNodeConfigFieldsProviderDiscordBot.tsx │ │ │ ├── BizNotifyNodeConfigFieldsProviderEmail.tsx │ │ │ ├── BizNotifyNodeConfigFieldsProviderMattermost.tsx │ │ │ ├── BizNotifyNodeConfigFieldsProviderSlackBot.tsx │ │ │ ├── BizNotifyNodeConfigFieldsProviderTelegramBot.tsx │ │ │ ├── BizNotifyNodeConfigFieldsProviderWebhook.tsx │ │ │ ├── BizNotifyNodeConfigForm.tsx │ │ │ ├── BizUploadNodeConfigDrawer.tsx │ │ │ ├── BizUploadNodeConfigForm.tsx │ │ │ ├── BranchBlockNodeConfigDrawer.tsx │ │ │ ├── BranchBlockNodeConfigExprInputBox.tsx │ │ │ ├── BranchBlockNodeConfigForm.tsx │ │ │ ├── DelayNodeConfigDrawer.tsx │ │ │ ├── DelayNodeConfigForm.tsx │ │ │ ├── StartNodeConfigDrawer.tsx │ │ │ ├── StartNodeConfigForm.tsx │ │ │ ├── _context.ts │ │ │ └── _shared.tsx │ │ ├── index.ts │ │ └── nodes/ │ │ ├── BizApplyNodeRegistry.tsx │ │ ├── BizDeployNodeRegistry.tsx │ │ ├── BizMonitorNodeRegistry.tsx │ │ ├── BizNotifyNodeRegistry.tsx │ │ ├── BizUploadNodeRegistry.tsx │ │ ├── ConditionNode.tsx │ │ ├── DelayNode.tsx │ │ ├── EndNode.tsx │ │ ├── StartNode.tsx │ │ ├── TryCatchNode.tsx │ │ ├── _example.ts │ │ ├── _shared.tsx │ │ ├── index.ts │ │ └── typings.ts │ ├── domain/ │ │ ├── access.ts │ │ ├── app.ts │ │ ├── certificate.ts │ │ ├── provider.ts │ │ ├── settings.ts │ │ ├── statistics.ts │ │ ├── workflow.ts │ │ ├── workflowLog.ts │ │ └── workflowRun.ts │ ├── global.css │ ├── hooks/ │ │ ├── index.ts │ │ ├── useAntdForm.ts │ │ ├── useAntdFormName.ts │ │ ├── useAppSettings.ts │ │ ├── useBrowserTheme.ts │ │ ├── useTriggerElement.ts │ │ ├── useVersionChecker.ts │ │ └── useZustandShallowSelector.ts │ ├── i18n/ │ │ ├── index.ts │ │ └── locales/ │ │ ├── en/ │ │ │ ├── index.ts │ │ │ ├── nls.access.json │ │ │ ├── nls.certificate.json │ │ │ ├── nls.common.json │ │ │ ├── nls.dashboard.json │ │ │ ├── nls.login.json │ │ │ ├── nls.preset.json │ │ │ ├── nls.provider.json │ │ │ ├── nls.settings.json │ │ │ ├── nls.workflow.json │ │ │ ├── nls.workflow.nodes.json │ │ │ ├── nls.workflow.runs.json │ │ │ └── nls.workflow.vars.json │ │ ├── index.ts │ │ └── zh/ │ │ ├── index.ts │ │ ├── nls.access.json │ │ ├── nls.certificate.json │ │ ├── nls.common.json │ │ ├── nls.dashboard.json │ │ ├── nls.login.json │ │ ├── nls.preset.json │ │ ├── nls.provider.json │ │ ├── nls.settings.json │ │ ├── nls.workflow.json │ │ ├── nls.workflow.nodes.json │ │ ├── nls.workflow.runs.json │ │ └── nls.workflow.vars.json │ ├── index.css │ ├── main.tsx │ ├── pages/ │ │ ├── AuthLayout.tsx │ │ ├── ConsoleLayout.tsx │ │ ├── ErrorLayout.tsx │ │ ├── accesses/ │ │ │ ├── AccessList.tsx │ │ │ └── AccessNew.tsx │ │ ├── certificates/ │ │ │ └── CertificateList.tsx │ │ ├── dashboard/ │ │ │ └── Dashboard.tsx │ │ ├── login/ │ │ │ └── Login.tsx │ │ ├── presets/ │ │ │ ├── PresetList.tsx │ │ │ ├── PresetListNotifyTemplates.tsx │ │ │ └── PresetListScriptTemplates.tsx │ │ ├── settings/ │ │ │ ├── Settings.tsx │ │ │ ├── SettingsAbout.tsx │ │ │ ├── SettingsAccount.tsx │ │ │ ├── SettingsAppearance.tsx │ │ │ ├── SettingsDiagnostics.tsx │ │ │ ├── SettingsPersistence.tsx │ │ │ └── SettingsSSLProvider.tsx │ │ └── workflows/ │ │ ├── WorkflowDetail.tsx │ │ ├── WorkflowDetailDesign.tsx │ │ ├── WorkflowDetailRuns.tsx │ │ ├── WorkflowList.tsx │ │ └── WorkflowNew.tsx │ ├── repository/ │ │ ├── _pocketbase.ts │ │ ├── access.ts │ │ ├── admin.ts │ │ ├── certificate.ts │ │ ├── settings.ts │ │ ├── system.ts │ │ ├── workflow.ts │ │ ├── workflowLog.ts │ │ └── workflowRun.ts │ ├── routers/ │ │ └── index.tsx │ ├── stores/ │ │ ├── access/ │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── settings/ │ │ │ ├── contact/ │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ ├── persistence/ │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── sslprovider/ │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ └── template/ │ │ │ ├── index.ts │ │ │ └── types.ts │ │ └── workflow/ │ │ ├── index.ts │ │ └── types.ts │ └── utils/ │ ├── browser.ts │ ├── cron.ts │ ├── css.ts │ ├── error.ts │ ├── file.ts │ ├── search.ts │ ├── validator.ts │ └── x509.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── types/ │ ├── global.d.ts │ ├── global.utility.d.ts │ ├── shims-antd.d.ts │ └── vite-env.d.ts └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ vendor ui/node_modules pb_data build .vscode ================================================ FILE: .editorconfig ================================================ # http://editorconfig.org root = true [*] charset = utf-8 end_of_line = crlf indent_size = 2 indent_style = space trim_trailing_whitespace = true insert_final_newline = true [*.go] charset = utf-8 end_of_line = lf indent_size = 2 indent_style = tab ================================================ FILE: .gitattributes ================================================ * text=auto eol=crlf *.go text eol=lf ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry polar: # Replace with a single Polar username buy_me_a_coffee: # Replace with a single Buy Me a Coffee username thanks_dev: # Replace with a single thanks.dev username custom: ["https://profile.ikit.fun/sponsors/"] ================================================ FILE: .github/ISSUE_TEMPLATE/1-bug_report.yml ================================================ name: "🐞 Bug Report" description: "Create a report to help us improve. / 报告缺陷来帮助我们完善。" title: "[Bug] Describe the Bug briefly / 简要描述你发现的缺陷" type: bug labels: - bug body: - type: markdown attributes: value: | **Before you submit the issue, please make sure of the following checklist**: 1. Yes, I'm using the latest release and can reproduce the issue. Issues that are not in the latest version will be closed directly. 2. Yes, I've searched for [existing issues](https://github.com/certimate-go/certimate/issues) (including closed ones) on GitHub and didn't find any similar. 3. Yes, I've read the [documentation](https://docs.certimate.me/) and didn't find any similar. 4. Please describe the problem in detail according to the template specification, otherwise the issue will be closed directly. 5. Please limit one report per issue. **在提交 Issue 之前,请确认以下事项**: 1. 我**确认**已尝试过使用当前最新版本,并能复现问题。由于开发者精力有限,非当前最新版本的问题将被直接关闭,感谢理解。 2. 我**确认**已搜索过[已有的 Issues](https://github.com/certimate-go/certimate/issues)(包括已关闭的),没有类似的问题。 3. 我**确认**已阅读过[文档](https://docs.certimate.me/),没有类似的问题。 4. 请**务必**按照模板规范详细描述问题,否则 Issue 将会被直接关闭。 5. 请保持每个 Issue 只包含一个缺陷报告。如果有多个缺陷,请分别提交 Issue。 - type: input attributes: label: Release Version / 软件版本 description: Please provide the specific version of Certimate. / 请提供 Certimate 的具体版本(请不要填写 `latest` 之类的无效版本号)。 placeholder: (e.g. v1.0.0. `latest` is **NOT** a valid version!) validations: required: true - type: textarea attributes: label: Description / 缺陷描述 description: Describe the bug you found in detail and clearly, and upload screenshots if possible. / 请详细清晰地描述你发现的缺陷或故障,如果可能请上传截图。 validations: required: true - type: textarea attributes: label: Steps to reproduce / 复现步骤 description: Please walk us through it step by step. / 请提供可复现的完整步骤。 placeholder: | 1. ... 2. ... 3. ... ... validations: required: true - type: textarea attributes: label: Logs / 日志 description: Add logs here if available. / 在此处添加日志信息(如果有的话)。 value: |-
```console # Paste logs here / 请在此粘贴日志 ```
validations: required: false - type: textarea attributes: label: Miscellaneous / 其他 description: Add any other context about the issue here. / 在此处添加关于该 Issue 的任何其他信息。 validations: required: false - type: checkboxes attributes: label: Contribution / 贡献代码 options: - label: I am interested in contributing a PR for this! / 我乐意为此提交代码并发起 PR! required: false ================================================ FILE: .github/ISSUE_TEMPLATE/2-feature_request.yml ================================================ name: "💡 Feature Request" description: "Suggest an idea for this project. / 提出新功能请求或改进意见。" title: "[Feature] Describe the feature briefly / 简要描述你希望实现的功能" type: feature labels: - enhancement body: - type: markdown attributes: value: | **Before you submit the issue, please make sure of the following checklist**: 1. Yes, I'm using the latest release. 2. Yes, I've searched for [existing issues](https://github.com/certimate-go/certimate/issues) (including closed ones) on GitHub and didn't find any similar. 3. Yes, I've read the [documentation](https://docs.certimate.me/) and didn't find any similar. 4. Please describe the problem in detail according to the template specification, otherwise the issue will be closed directly. 5. Please limit one request per issue. **在提交 Issue 之前,请确认以下事项**: 1. 我**确认**是基于当前最新大版本而提出的新功能请求或改进意见。 2. 我**确认**已搜索过[已有的 Issues](https://github.com/certimate-go/certimate/issues)(包括已关闭的),没有类似的问题。 3. 我**确认**已阅读过[文档](https://docs.certimate.me/),没有类似的问题。 4. 请**务必**按照模板规范详细描述问题,否则 Issue 将会被直接关闭。 5. 请保持每个 Issue 只包含一个功能请求。如果有多个需求,请分别提交 Issue。 - type: textarea attributes: label: Description / 功能描述 description: Describe the feature you'd like to add in detail and clearly, and upload screenshots if possible. / 请详细清晰地描述你希望添加的功能,如果可能请上传截图。 validations: required: true - type: textarea attributes: label: Motivation / 请求动机 description: Why is this feature helpful to the project? / 为什么这个功能对项目有帮助? validations: required: true - type: textarea attributes: label: Miscellaneous / 其他 description: Add any other context about the problem here. / 在此处添加关于该 Issue 的任何其他信息(新增提供商请求请补充 API 文档链接等资料)。 validations: required: false - type: checkboxes attributes: label: Contribution / 贡献代码 options: - label: I am interested in contributing a PR for this! / 我乐意为此提交代码并发起 PR! required: false ================================================ FILE: .github/ISSUE_TEMPLATE/3-questions.yml ================================================ name: "❓ Questions" description: "Have problem in use and need help? / 遇到了困难需要求助?" title: "Describe the question briefly / 简要描述你遇到的问题" type: question body: - type: markdown attributes: value: | **Before you submit the issue, please make sure of the following checklist**: 1. Yes, I'm using the latest release. 2. Yes, I've searched for [existing issues](https://github.com/certimate-go/certimate/issues) (including closed ones) on GitHub and didn't find any similar. 3. Yes, I've read the [documentation](https://docs.certimate.me/) and didn't find any similar. 4. Please describe the problem in detail according to the template specification, otherwise the issue will be closed directly. 5. Please limit one question per issue. **在提交 Issue 之前,请确认以下事项**: 1. 我**确认**正在使用的是当前最新版本。 2. 我**确认**已搜索过[已有的 Issues](https://github.com/certimate-go/certimate/issues)(包括已关闭的),没有类似的问题。 3. 我**确认**已阅读过[文档](https://docs.certimate.me/),没有类似的问题。 4. 请**务必**按照模板规范详细描述问题,否则 Issue 将会被直接关闭。 5. 请保持每个 Issue 只包含一个问题求助。如果有多个问题,请分别提交 Issue。 - type: input attributes: label: Release Version / 软件版本 description: Please provide the specific version of Certimate. / 请提供 Certimate 的具体版本(请不要填写 `latest` 之类的无效版本号)。 placeholder: (e.g. v1.0.0) validations: required: true - type: textarea attributes: label: Description / 问题描述 description: Describe the problem you encountered in detail and clearly, and upload screenshots if possible. / 请详细清晰地描述你遇到的问题,如果可能请上传截图。 validations: required: true - type: textarea attributes: label: Miscellaneous / 其他 description: Add any other context about the problem here. / 在此处添加关于该问题的任何其他信息。 validations: required: false ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: "🌐 Community / 社群讨论" about: "Join in our Telegram channel. / 加入到电报频道寻求更多帮助。" url: "https://t.me/+ZXphsppxUg41YmVl" - name: "📖 FAQ / 常见问题" about: "Please take a look to FAQs. / 请先阅读文档 FAQ,可能会有你需要的答案。" url: "https://docs.certimate.me/docs/reference/faq" ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================  ================================================ FILE: .github/workflows/push_image.yml ================================================ name: Docker Image CI (stable versions) on: push: tags: - "v[0-9]*" - "!v*alpha*" - "!v*beta*" - "!v*rc*" workflow_dispatch: inputs: tag: description: "Tag version to be used for Docker image" required: true default: "latest" jobs: prepare-ui: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Node.js uses: actions/setup-node@v6 with: node-version: 24 - name: Build UI run: | npm --prefix=./ui ci npm --prefix=./ui run build - name: Upload UI build artifacts uses: actions/upload-artifact@v5 with: name: ui-build path: ./ui/dist retention-days: 1 build-and-push: needs: prepare-ui runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 - name: Free disk space uses: BRAINSia/free-disk-space@v2 with: tool-cache: false - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Docker meta id: meta uses: docker/metadata-action@v5 with: images: | certimate/certimate registry.cn-shanghai.aliyuncs.com/certimate/certimate tags: | type=ref,event=branch type=ref,event=pr type=semver,pattern=v{{version}} type=semver,pattern=v{{major}}.{{minor}} - name: Log in to DOCKERHUB uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Log in to ALIYUNCS uses: docker/login-action@v3 with: registry: registry.cn-shanghai.aliyuncs.com username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Download UI build artifacts uses: actions/download-artifact@v6 with: name: ui-build path: ./ui/dist - name: Build and push Docker image uses: docker/build-push-action@v6 with: context: . # file: ./Dockerfile file: ./Dockerfile.gh platforms: linux/amd64,linux/arm64,linux/arm/v7 push: true tags: ${{ steps.meta.outputs.tags }} ================================================ FILE: .github/workflows/push_image_next.yml ================================================ name: Docker Image CI (preview versions) on: push: tags: - "v[0-9]*-alpha*" - "v[0-9]*-beta*" - "v[0-9]*-rc*" workflow_dispatch: inputs: tag: description: "Tag version to be used for Docker image" required: true default: "next" jobs: prepare-ui: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Node.js uses: actions/setup-node@v6 with: node-version: 24 - name: Build UI run: | npm --prefix=./ui ci npm --prefix=./ui run build - name: Upload UI build artifacts uses: actions/upload-artifact@v5 with: name: ui-build path: ./ui/dist retention-days: 1 build-and-push: needs: prepare-ui runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 - name: Free disk space uses: BRAINSia/free-disk-space@v2 with: tool-cache: false - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Docker meta id: meta uses: docker/metadata-action@v5 with: images: | certimate/certimate registry.cn-shanghai.aliyuncs.com/certimate/certimate tags: | type=ref,event=tag,pattern={{version}} type=raw,value=next flavor: | latest=false - name: Log in to DOCKERHUB uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Log in to ALIYUNCS uses: docker/login-action@v3 with: registry: registry.cn-shanghai.aliyuncs.com username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Download UI build artifacts uses: actions/download-artifact@v6 with: name: ui-build path: ./ui/dist - name: Build and push Docker image uses: docker/build-push-action@v6 with: context: . # file: ./Dockerfile file: ./Dockerfile.gh platforms: linux/amd64,linux/arm64,linux/arm/v7 push: true tags: ${{ steps.meta.outputs.tags }} ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: tags: - "v[0-9]*" jobs: prepare-ui: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Node.js uses: actions/setup-node@v6 with: node-version: 24 - name: Build UI run: | npm --prefix=./ui ci npm --prefix=./ui run build - name: Upload UI build artifacts uses: actions/upload-artifact@v5 with: name: ui-build path: ./ui/dist retention-days: 1 build-linux: needs: prepare-ui runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: "go.mod" - name: Download UI build artifacts uses: actions/download-artifact@v6 with: name: ui-build path: ./ui/dist - name: Build Linux binaries env: CGO_ENABLED: 0 GOOS: linux run: | mkdir -p dist/linux for ARCH in amd64 arm64 armv7; do if [ "$ARCH" == "armv7" ]; then go env -w GOARCH=arm go env -w GOARM=7 else go env -w GOARCH=$ARCH go env -u GOARM fi go build -trimpath -ldflags="-s -w" -o dist/linux/certimate_${GITHUB_REF#refs/tags/}_linux_$ARCH done - name: Upload Linux binaries uses: actions/upload-artifact@v5 with: name: linux-binaries path: dist/linux/ retention-days: 1 build-macos: needs: prepare-ui runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: "go.mod" - name: Download UI build artifacts uses: actions/download-artifact@v6 with: name: ui-build path: ./ui/dist - name: Build macOS binaries env: CGO_ENABLED: 0 GOOS: darwin run: | mkdir -p dist/darwin for ARCH in amd64 arm64; do go env -w GOARCH=$ARCH go build -trimpath -ldflags="-s -w" -o dist/darwin/certimate_${GITHUB_REF#refs/tags/}_darwin_$ARCH done - name: Upload macOS binaries uses: actions/upload-artifact@v5 with: name: macos-binaries path: dist/darwin/ retention-days: 1 build-windows: needs: prepare-ui runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: "go.mod" - name: Download UI build artifacts uses: actions/download-artifact@v6 with: name: ui-build path: ./ui/dist - name: Build Windows binaries env: CGO_ENABLED: 0 GOOS: windows run: | mkdir -p dist/windows for ARCH in amd64 arm64 386; do go env -w GOARCH=$ARCH go build -trimpath -ldflags="-s -w" -o dist/windows/certimate_${GITHUB_REF#refs/tags/}_windows_$ARCH.exe done - name: Upload Windows binaries uses: actions/upload-artifact@v5 with: name: windows-binaries path: dist/windows/ retention-days: 1 create-release: needs: [build-linux, build-macos, build-windows] runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Download all binaries uses: actions/download-artifact@v6 with: path: ./artifacts - name: Prepare release assets run: | mkdir -p dist cp -r artifacts/linux-binaries/* dist/ cp -r artifacts/macos-binaries/* dist/ cp -r artifacts/windows-binaries/* dist/ find dist -type f -not -name "*.exe" -exec chmod +x {} \; cd dist for bin in certimate_*; do if [[ "$bin" == *".exe" ]]; then entrypoint="certimate.exe" else entrypoint="certimate" fi tmpdir=$(mktemp -d) cp "$bin" "${tmpdir}/${entrypoint}" cp ../LICENSE "$tmpdir/LICENSE" cp ../README.md "$tmpdir/README.md" cp ../CHANGELOG.md "$tmpdir/CHANGELOG.md" if [[ "$bin" == *".exe" ]]; then zip -j "${bin%.exe}.zip" "$tmpdir"/* else zip -j -X "${bin}.zip" "$tmpdir"/* fi rm -rf "$tmpdir" done sha256sum *.zip > checksums.txt - name: Create Release uses: softprops/action-gh-release@v2 with: files: | dist/*.zip dist/checksums.txt draft: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/release_sync_gitee.py ================================================ #!/usr/bin/env python3 import logging import json import mimetypes import tempfile import os import random import re import shutil import time from urllib import request from urllib.error import HTTPError GITHUB_REPO = "certimate-go/certimate" GITEE_REPO = "certimate-go/certimate" GITEE_TOKEN = os.getenv("GITEE_TOKEN", "") SYNC_MARKER = "SYNCING FROM GITHUB, PLEASE WAIT ..." TEMP_DIR = tempfile.mkdtemp() logging.basicConfig(level=logging.INFO) def do_httpreq(url, method="GET", headers=None, data=None): req = request.Request(url, data=data, method=method) headers = headers or {} for key, value in headers.items(): req.add_header(key, value) try: with request.urlopen(req) as resp: resp_data = resp.read().decode("utf-8") if resp_data: try: return json.loads(resp_data) except json.JSONDecodeError: pass return None except HTTPError as e: errmsg = "" if e.readable(): try: errmsg = e.read().decode('utf-8') errmsg = errmsg.replace("\r", "\\r").replace("\n", "\\n") except: pass logging.error(f"Error occurred when sending request: status={e.status}, response={errmsg}") raise e except Exception as e: raise e def get_github_stable_release(): page = 1 while True: releases = do_httpreq( url=f"https://api.github.com/repos/{GITHUB_REPO}/releases?page={page}&per_page=100", headers={"Accept": "application/vnd.github+json"}, ) if not releases or len(releases) == 0: break for release in releases: release_name = release.get("name", "") if re.match(r"^v[0-9]", release_name): if any( x in release_name for x in ["alpha", "beta", "rc", "preview", "test", "unstable"] ): continue return release page += 1 return None def get_gitee_release_list(): page = 1 list = [] while True: releases = do_httpreq( url=f"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases?access_token={GITEE_TOKEN}&page={page}&per_page=100", ) if not releases or len(releases) == 0: break list.extend(releases) page += 1 return list def get_gitee_release_by_tag(tag_name): releases = get_gitee_release_list() for release in releases: if release.get("tag_name") == tag_name: return release return None def delete_gitee_release(release_info): if not release_info: raise ValueError("Release info is invalid") release_id = release_info.get("id", "") release_name = release_info.get("tag_name", "") if not release_id: raise ValueError("Release ID is missing") attachpage = 1 attachfiles = [] while True: releases = do_httpreq( url=f"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases/{release_id}/attach_files?access_token={GITEE_TOKEN}&page={attachpage}&per_page=100", ) if not releases or len(releases) == 0: break attachfiles.extend(releases) attachpage += 1 for attachfile in attachfiles: attachfile_id = attachfile.get("id") attachfile_name = attachfile.get("name") logging.info("Trying to delete Gitee attach file: %s/%s", release_name, attachfile_name) do_httpreq( url=f"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases/{release_id}/attach_files/{attachfile_id}?access_token={GITEE_TOKEN}", method="DELETE", ) logging.info("Trying to delete Gitee release: %s", release_name) do_httpreq( url=f"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases/{release_id}?access_token={GITEE_TOKEN}", method="DELETE", ) def create_gitee_release(name, tag, body, prerelease, gh_assets): release_info = do_httpreq( f"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases?access_token={GITEE_TOKEN}", method="POST", headers={"Content-Type": "application/json"}, data=json.dumps({ "tag_name": tag, "name": name, "body": SYNC_MARKER, "prerelease": prerelease, "target_commitish": "", }).encode("utf-8"), ) if not release_info or "id" not in release_info: return None logging.info("Gitee release created") release_id = release_info["id"] assets_dir = os.path.join(TEMP_DIR, "assets") os.makedirs(assets_dir, exist_ok=True) gh_assets = gh_assets or [] for asset in gh_assets: logging.info("Tring to download asset from GitHub: %s", asset["name"]) opener = request.build_opener() request.install_opener(opener) download_ts = time.time() download_url = asset.get("browser_download_url") download_path = os.path.join(assets_dir, asset["name"]) def _hook(blocknum, blocksize, totalsize): nonlocal download_ts TIMESPAN = 5 # print progress every 5sec ts = time.time() pct = min(round(100 * blocknum * blocksize / totalsize, 2), 100) if (ts - download_ts < TIMESPAN) and (pct < 100): return download_ts = ts logging.info(f"Downloading {download_url} >>> {pct}%") request.urlretrieve(download_url, download_path, _hook) for asset in gh_assets: logging.info("Tring to upload asset to Gitee: %s", asset["name"]) boundary = '----boundary' + ''.join(random.choice('0123456789abcdef') for _ in range(16)) print(f"Using boundary: {boundary}") with open(os.path.join(assets_dir, asset["name"]), 'rb') as f: attachfile_mime = mimetypes.guess_type(asset["name"])[0] or 'application/octet-stream' attachfile_req = [] attachfile_req.append(f"--{boundary}") attachfile_req.append(f'Content-Disposition: form-data; name="file"; filename="{asset["name"]}"') attachfile_req.append(f"Content-Type: {attachfile_mime}") attachfile_req.append("") attachfile_req.append(f.read().decode('latin-1')) attachfile_req.append(f"--{boundary}--") attachfile_req.append("") attachfile_req = "\r\n".join(attachfile_req).encode('latin-1') do_httpreq( f"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases/{release_id}/attach_files?access_token={GITEE_TOKEN}", method="POST", headers={'Content-Type': f'multipart/form-data; boundary={boundary}'}, data=attachfile_req, ) logging.info("Asset uploaded: %s", asset["name"]) release_info = do_httpreq( f"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases/{release_id}?access_token={GITEE_TOKEN}", method="PATCH", headers={"Content-Type": "application/json"}, data=json.dumps({ "tag_name": tag, "name": name, "body": f"**此发行版同步自 GitHub,完整变更日志请访问 https://github.com/{GITHUB_REPO}/releases/{tag} 查看。**\n\n**因 Gitee 存储空间容量有限,仅能保留最新一个发行版,如需其余版本请访问 GitHub 获取。**\n\n---\n\n" + body, "prerelease": prerelease, }).encode("utf-8"), ) logging.info("Gitee release updated") return release_info def main(): try: # 获取 GitHub 最新稳定发行版 github_release = get_github_stable_release() if not github_release: logging.warning("GitHub stable release not found. Foget to release?") return else: logging.info("GitHub stable release found: %s", github_release.get('name')) # 提取稳定版的信息 release_name = github_release.get("name") release_tag = github_release.get("tag_name") release_body = github_release.get("body") release_prerelease = github_release.get("prerelease", False) release_assets = github_release.get("assets", []) # 检查 Gitee 是否已有同名发行版 gitee_release = get_gitee_release_by_tag(release_tag) if gitee_release and gitee_release.get("body") == SYNC_MARKER: logging.warning("Gitee syncing release found, cleaning up...") delete_gitee_release(gitee_release) elif gitee_release: logging.info("Gitee release already exists, exit.") return # 同步发行版 gitee_release = create_gitee_release(release_name, release_tag, release_body, release_prerelease, release_assets) if not gitee_release: logging.warning("Failed to create Gitee release.") return # 清除历史发行版 gitee_release_list = get_gitee_release_list() for release in gitee_release_list: if release.get("tag_name") == release_tag: continue else: delete_gitee_release(release) logging.info("Sync release completed.") except Exception as e: logging.fatal(str(e)) exit(1) finally: if os.path.exists(TEMP_DIR): shutil.rmtree(TEMP_DIR) if __name__ == "__main__": main() ================================================ FILE: .github/workflows/release_sync_gitee.yml ================================================ name: Release Sync to Gitee on: # release: # types: [published, unpublished, deleted] workflow_dispatch: jobs: sync-to-gitee: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Python3 uses: actions/setup-python@v6 with: python-version: "3.13" - name: Run script env: GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }} run: | cd .github/workflows python ./release_sync_gitee.py ================================================ FILE: .gitignore ================================================ .vscode/* !.vscode/extensions.json !.vscode/settings.json !.vscode/settings.tailwind.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? __debug_bin* vendor pb_data build main /dist /docker/data /certimate ================================================ FILE: .goreleaser.yml ================================================ project_name: certimate dist: .builds before: hooks: - go mod tidy builds: - id: build_noncgo main: ./ binary: certimate env: - CGO_ENABLED=0 flags: - -trimpath ldflags: - -s -w -X github.com/certimate-go/certimate.Version={{ .Version }} goos: - linux - windows - darwin goarch: - amd64 - arm64 - arm goarm: - 7 ignore: - goos: windows goarch: arm - goos: darwin goarch: arm # upx: # - enabled: true release: draft: true archives: - id: archive_noncgo builds: [build_noncgo] format: "zip" files: - LICENSE - README.md - CHANGELOG.md checksum: name_template: "checksums.txt" snapshot: name_template: "{{ incpatch .Version }}-next" changelog: sort: asc filters: exclude: - "^ui:" ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ "bradlc.vscode-tailwindcss", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", ] } ================================================ FILE: .vscode/settings.json ================================================ { "css.customData": [ ".vscode/settings.tailwind.json" ], "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, "editor.defaultFormatter": "dbaeumer.vscode-eslint", "editor.formatOnSave": true, "go.useLanguageServer": true, "gopls": { "formatting.gofumpt": true, }, "typescript.tsdk": "ui/node_modules/typescript/lib", "[go]": { "editor.defaultFormatter": "golang.go" }, "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" } } ================================================ FILE: .vscode/settings.tailwind.json ================================================ { "version": 1.1, "atDirectives": [ { "name": "@apply", "description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS.", "references": [ { "name": "Tailwind Documentation", "url": "https://tailwindcss.com/docs/functions-and-directives#variant-directive" } ] }, { "name": "@source", "description": "Use the `@source` directive to explicitly specify source files that aren't picked up by Tailwind's automatic content detection.", "references": [ { "name": "Tailwind Documentation", "url": "https://tailwindcss.com/docs/functions-and-directives#source-directive" } ] }, { "name": "@theme", "description": "Use the `@theme` directive to define your project's custom design tokens, like fonts, colors, and breakpoints.", "references": [ { "name": "Tailwind Documentation", "url": "https://tailwindcss.com/docs/functions-and-directives#theme-directive" } ] }, { "name": "@utility", "description": "Use the `@utility` directive to add custom utilities to your project that work with variants like `hover`, `focus` and `lg`.", "references": [ { "name": "Tailwind Documentation", "url": "https://tailwindcss.com/docs/functions-and-directives#utility-directive" } ] }, { "name": "@variant", "description": "Use the `@variant` directive to apply a Tailwind variant to styles in your CSS", "references": [ { "name": "Tailwind Documentation", "url": "https://tailwindcss.com/docs/functions-and-directives#variant-directive" } ] } ] } ================================================ FILE: CHANGELOG.md ================================================ A full changelog of past releases is available on [GitHub Releases](https://github.com/certimate-go/certimate/releases) page. ================================================ FILE: CONTRIBUTING.md ================================================ # Contribution Guide
English | [简体中文](CONTRIBUTING_zh.md)
Thank you for taking the time to improve Certimate! Below is a guide for submitting a PR (Pull Request) to the Certimate repository. We need to be nimble and ship fast given where we are, but we also want to make sure that contributors like you get as smooth an experience at contributing as possible. We've assembled this contribution guide for that purpose, aiming at getting you familiarized with the codebase & how we work with contributors, so you could quickly jump to the fun part. Index: - [Development](#development) - [Prerequisites](#prerequisites) - [Backend Code](#backend-code) - [Frontend Code](#frontend-code) - [Submitting PR](#submitting-pr) - [Pull Request Process](#pull-request-process) - [Getting Help](#getting-help) --- ## Development ### Prerequisites - Go 1.25+ (for backend code changes) - Node.js 22.12+ (for frontend code changes) ### Backend Code The backend code of Certimate is developed using Golang. It is a monolithic application based on [Pocketbase](https://github.com/pocketbase/pocketbase). Once you have made changes to the backend code in Certimate, follow these steps to run the project: 1. Navigate to the root directory. 2. Install dependencies: ```bash go mod vendor ``` 3. Start the local development server: ```bash go run main.go serve ``` This will start a web server at `http://localhost:8090` using the prebuilt WebUI located in `/ui/dist`. > If you encounter an error `ui/embed.go: pattern all:dist: no matching files found`, please refer to _[Frontend Code](#frontend-code)_ and build WebUI first. **Before submitting a PR to the main repository, you should:** - Format your source code by using [gofumpt](https://github.com/mvdan/gofumpt). Recommended using VSCode and installing the gofumpt plugin to automatically format when saving. - Adding unit or integration tests for your changes (with go standard library `testing` package). ### Frontend Code The frontend code of Certimate is developed using TypeScript. It is a SPA based on [React](https://github.com/facebook/react) and [Vite](https://github.com/vitejs/vite). Once you have made changes to the backend code in Certimate, follow these steps to run the project: 1. Navigate to the `/ui` directory. 2. Install dependencies: ```bash npm install ``` 3. Start the local development server: ```bash npm run dev ``` This will start a web server at `http://localhost:5173`. You can now access the WebUI in your browser. After completing your changes, build the WebUI so it can be embedded into the Go package: ```bash npm run build ``` **Before submitting a PR to the main repository, you should:** - Format your source code by using [ESLint](https://github.com/eslint/eslint). Recommended using VSCode and installing the ESLint plugin to automatically format when saving. ## Submitting PR Before opening a Pull Request, please open an issue to discuss the change and get feedback from the maintainers. This will helps us: - To understand the context of the change. - To ensure it fits into Certimate's roadmap. - To prevent us from duplicating work. - To prevent you from spending time on a change that we may not be able to accept. ### Pull Request Process 1. Fork the repository, and then checkout `main` branch. 2. Before you draft a PR, please open an issue to discuss the changes you want to make. 3. Create a new branch for your changes. 4. Please add tests for your changes accordingly. 5. Ensure your code passes the existing tests. 6. Please link the issue in the PR description. 7. Get merged! > [!IMPORTANT] > > It is recommended to create a new branch from `main` for each bug fix or feature. If you plan to submit multiple PRs, ensure the changes are in separate branches for easier review and eventual merge. > > Keep each PR focused on a single feature or fix. ## Getting Help If you ever get stuck or get a burning question while contributing, simply shoot your queries our way via the GitHub issues. ================================================ FILE: CONTRIBUTING_zh.md ================================================ # 贡献指南
[English](CONTRIBUTING.md) | 简体中文
非常感谢你抽出时间来帮助改进 Certimate!以下是向 Certimate 提交 Pull Request 时的操作指南。 我们需要保持敏捷和快速迭代,同时也希望确保贡献者能获得尽可能流畅的参与体验。这份贡献指南旨在帮助你熟悉代码库和我们的工作方式,让你可以尽快进入有趣的开发环节。 索引: - [开发](#开发) - [要求](#要求) - [后端代码](#后端代码) - [前端代码](#前端代码) - [提交 PR](#提交-pr) - [提交流程](#提交流程) - [获取帮助](#获取帮助) --- ## 开发 ### 要求 - Go 1.25+(用于修改后端代码) - Node.js 22.12+(用于修改前端代码) ### 后端代码 Certimate 的后端代码是使用 Golang 开发的,是一个基于 [Pocketbase](https://github.com/pocketbase/pocketbase) 构建的单体应用。 假设你已经对 Certimate 的后端代码做出了一些修改,现在你想要运行它,请遵循以下步骤: 1. 进入根目录; 2. 安装依赖: ```bash go mod vendor ``` 3. 启动本地开发服务: ```bash go run main.go serve ``` 这将启动一个 Web 服务器,默认运行在 `http://localhost:8090`,并使用来自 `/ui/dist` 的预构建管理页面。 > 如果你遇到报错 `ui/embed.go: pattern all:dist: no matching files found`,请参考“[前端代码](#前端代码)”这一小节构建 WebUI。 **在向主仓库提交 PR 之前,你应该:** - 使用 [gofumpt](https://github.com/mvdan/gofumpt) 格式化你的代码。推荐使用 VSCode,并安装 gofumpt 插件,以便在保存时自动格式化。 - 为你的改动添加单元测试或集成测试(使用 Go 标准库中的 `testing` 包)。 ### 前端代码 Certimate 的前端代码是使用 TypeScript 开发的,是一个基于 [React](https://github.com/facebook/react) 和 [Vite](https://github.com/vitejs/vite) 构建的单页应用。 假设你已经对 Certimate 的前端代码做出了一些修改,现在你想要运行它,请遵循以下步骤: 1. 进入 `/ui` 目录; 2. 安装依赖: ```bash npm install ``` 3. 启动 Vite 开发服务器: ```bash npm run dev ``` 这将启动一个 Web 服务器,默认运行在 `http://localhost:5173`,你可以通过浏览器访问来查看运行中的 WebUI。 完成修改后,运行以下命令来构建 WebUI,以便它能被嵌入到 Go 包中: ```bash npm run build ``` **在向主仓库提交 PR 之前,你应该:** - 使用 [ESLint](https://github.com/eslint/eslint) 格式化你的代码。推荐使用 VSCode,并安装 ESLint 插件,以便在保存时自动格式化。 ## 提交 PR 在提交 PR 之前,请先创建一个 Issue 来讨论你的修改方案,并等待来自项目维护者的反馈。这样做有助于: - 让我们充分理解你的修改内容; - 评估修改是否符合项目路线图; - 避免重复工作; - 防止你投入时间到可能无法被合并的修改中。 ### 提交流程 1. Fork 本仓库并签出到 `main` 分支; 2. 在提交 PR 之前,请先发起 Issue 讨论你想要做的修改; 3. 为你的修改创建一个新的分支; 4. 请为你的修改添加相应的测试; 5. 确保你的代码能通过现有的测试; 6. 请在 PR 描述中关联相关 Issue; 7. 等待合并! > [!IMPORTANT] > > 建议为每个新功能或 Bug 修复创建一个从 `main` 分支派生的新分支。如果你计划提交多个 PR,请保持不同的改动在独立分支中,以便更容易进行代码审查并最终合并。 > > 保持一个 PR 只实现一个功能或修复。 ## 获取帮助 如果你在贡献过程中遇到困难或问题,可以通过 GitHub Issues 向我们提问。 ================================================ FILE: Dockerfile ================================================ FROM node:24-alpine AS webui-builder WORKDIR /app COPY . /app/ RUN \ cd /app/ui && \ npm install && \ npm run build FROM golang:1.25-alpine AS server-builder WORKDIR /app COPY ../. /app/ RUN rm -rf /app/ui/dist COPY --from=webui-builder /app/ui/dist /app/ui/dist ENV CGO_ENABLED=0 RUN go build -trimpath -ldflags="-s -w" -o certimate FROM alpine:latest WORKDIR /app COPY --from=server-builder /app/certimate . ENTRYPOINT ["./certimate", "serve", "--http", "0.0.0.0:8090"] ================================================ FILE: Dockerfile.gh ================================================ # Build docker image on GitHub Actions runner is too slow, # and it doesn't support armv7 (see https://github.com/parcel-bundler/lightningcss/issues/988). # So we pre-build webui, and just use simple Dockerfile here. FROM golang:1.25-alpine AS server-builder WORKDIR /app COPY ../. /app/ ENV CGO_ENABLED=0 RUN go build -trimpath -ldflags="-s -w" -o certimate FROM alpine:latest WORKDIR /app COPY --from=server-builder /app/certimate . ENTRYPOINT ["./certimate", "serve", "--http", "0.0.0.0:8090"] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2025 certimate-go Copyright (c) 2024 Yoan.Liu Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ # 定义变量 BINARY_NAME=certimate BUILD_DIR=build # 支持的操作系统和架构列表 OS_ARCH=\ linux/amd64 \ linux/arm64 \ darwin/amd64 \ darwin/arm64 \ windows/amd64 \ windows/arm64 # 默认目标 all: build # 构建所有平台的二进制文件 build: $(OS_ARCH) $(OS_ARCH): @mkdir -p $(BUILD_DIR) GOOS=$(word 1,$(subst /, ,$@)) \ GOARCH=$(word 2,$(subst /, ,$@)) \ CGO_ENABLED=0 \ go build -trimpath -ldflags="-s -w" -o $(BUILD_DIR)/$(BINARY_NAME)_$(word 1,$(subst /, ,$@))_$(word 2,$(subst /, ,$@)) . # 清理构建文件 clean: rm -rf $(BUILD_DIR) # 帮助信息 help: @echo "Usage:" @echo " make - 编译所有平台的二进制文件" @echo " make clean - 清理构建文件" @echo " make help - 显示此帮助信息" .PHONY: all build clean help local.run: go mod vendor&& npm --prefix=./ui install && npm --prefix=./ui run build && go run main.go serve --http 127.0.0.1:8090 ================================================ FILE: README.md ================================================

🔒 Certimate

[![Stars](https://img.shields.io/github/stars/certimate-go/certimate?style=flat)](https://github.com/certimate-go/certimate) [![Forks](https://img.shields.io/github/forks/certimate-go/certimate?style=flat)](https://github.com/certimate-go/certimate) [![Docker Pulls](https://img.shields.io/docker/pulls/certimate/certimate?style=flat)](https://hub.docker.com/r/certimate/certimate) [![Release](https://img.shields.io/github/v/release/certimate-go/certimate?style=flat&sort=semver)](https://github.com/certimate-go/certimate/releases) [![License](https://img.shields.io/github/license/certimate-go/certimate?style=flat)](https://mit-license.org/) [![Ask DeepWiki](https://deepwiki.com/badge.svg?label=DeepWiki)](https://deepwiki.com/certimate-go/certimate)
English | [简体中文](README_zh.md)
--- ## 🚩 Introduction An open-source and free self-hosted SSL certificates ACME tool, automates the full-cycle of issuance, deployment, renewal, and monitoring visually. - **Self-Hosted**: Private deployment. All data is stored locally, giving you full control to ensure data privacy and security. - **Zero Dependencies**: No need to install databases, runtimes, or any complex frameworks. Truly ready to use out of the box with a single click. - **Low Resource Usage**: Extremely lightweight, requiring only ~16 MB of memory. It's so efficient that it can even run on devices like home routers. - **Easy to Use**: The user-friendly GUI lets you automate certificate management for multiple platforms with a visual workflow — all with just a few simple configurations. ## 💡 Features - Flexible workflow orchestration, fully automation from certificate application to deployment. - Supports requesting single/multiple/wildcard domain certificates, IP address certificates, with options for RSA or ECC key. - Supports DNS-01 challenge and HTTP-01 challenge both. - Supports various certificate formats such as PEM, PFX, JKS. - Supports more than 60+ domain registrars (e.g., AWS, Cloudflare, GoDaddy, Alibaba Cloud, Tencent Cloud, etc. [Check out full providers](https://docs.certimate.me/en-US/docs/reference/providers#supported-dns-providers)). - Supports more than 110+ deployment targets (e.g., Kubernetes, CDN, WAF, load balancers, etc. [Check out full providers](https://docs.certimate.me/en-US/docs/reference/providers#supported-hosting-providers)). - Supports multiple notification channels including email, Discord, Slack, Telegram, DingTalk, Feishu, WeCom, and more. - Supports multiple ACME CAs including Let's Encrypt, Actalis, Google Trust Services, SSL.com, ZeroSSL, and more. - More features waiting to be discovered. ## 🚀 Quick Start **Run Certimate in 1 minute!**
👉 Binary Installation: Download the archived package of precompiled executable files directly from [GitHub Releases](https://github.com/certimate-go/certimate/releases), extract and then execute: ```bash ./certimate serve ```
👉 Docker Installation: ```bash docker run -d \ --name certimate \ --restart unless-stopped \ -p 8090:8090 \ -v /etc/localtime:/etc/localtime:ro \ -v /etc/timezone:/etc/timezone:ro \ -v $(pwd)/data:/app/pb_data \ certimate/certimate:latest ```
Visit `http://127.0.0.1:8090` in your browser. Default administrator account: - Username: `admin@certimate.fun` - Password: `1234567890` Work with Certimate right now. Or read other content in the documentation to learn more. ## 📄 Documentation For full documentation, please visit [docs.certimate.me](https://docs.certimate.me/). Related articles: > - [_Migrate to v0.4_](https://docs.certimate.me/en-US/docs/migrations/migrate-to-v0.4) > - [_使用 CNAME 完成 ACME DNS-01 质询_](https://docs.certimate.me/en-US/blog/cname) > - [_Why Certimate?_](https://docs.certimate.me/en-US/blog/why-certimate) ## 🖥️ Screenshot [![Screenshot](https://i.imgur.com/4DAUKEE.gif)](https://www.youtube.com/watch?v=am_yzdfyNOE) ## 🤝 Contributing Certimate is a free and open-source project, and your feedback and contributions are needed and always welcome. Contributions include but are not limited to: submitting code, reporting bugs, sharing ideas, or showcasing your use cases based on Certimate. We also encourage users to share Certimate on personal blogs or social media. For those who'd like to contribute code, see our [Contribution Guide](./CONTRIBUTING_EN.md). [Issues](https://github.com/certimate-go/certimate/issues) and [Pull Requests](https://github.com/certimate-go/certimate/pulls) are opened at https://github.com/certimate-go/certimate. #### Contributors [![Contributors](https://contrib.rocks/image?repo=certimate-go/certimate)](https://github.com/certimate-go/certimate/graphs/contributors) ## ⛔ Disclaimer This repository is available under the [MIT License](https://opensource.org/licenses/MIT), and distributed “as-is” without any warranty of any kind. The authors and contributors are not responsible for any damages or losses resulting from the use or inability to use this software, including but not limited to data loss, business interruption, or any other potential harm. **No Warranties**: This software comes without any express or implied warranties, including but not limited to implied warranties of merchantability, fitness for a particular purpose, and non-infringement. **User Responsibilities**: By using this software, you agree to take full responsibility for any outcomes resulting from its use. ## 🌐 Join the Community - [Telegram](https://t.me/+ZXphsppxUg41YmVl) - Wechat Group (contact to the author [@usual2970](https://github.com/usual2970) to getting invitation) ## ⭐ Star History Star Certificate on GitHub and be instantly notified of new releases. [![Stargazers over time](https://starchart.cc/certimate-go/certimate.svg?variant=adaptive)](https://starchart.cc/certimate-go/certimate) ================================================ FILE: README_zh.md ================================================

🔒 Certimate

[![Stars](https://img.shields.io/github/stars/certimate-go/certimate?style=flat)](https://github.com/certimate-go/certimate) [![Forks](https://img.shields.io/github/forks/certimate-go/certimate?style=flat)](https://github.com/certimate-go/certimate) [![Docker Pulls](https://img.shields.io/docker/pulls/certimate/certimate?style=flat)](https://hub.docker.com/r/certimate/certimate) [![Release](https://img.shields.io/github/v/release/certimate-go/certimate?style=flat&sort=semver)](https://github.com/certimate-go/certimate/releases) [![License](https://img.shields.io/github/license/certimate-go/certimate?style=flat)](https://mit-license.org/) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/certimate-go/certimate)
[English](README.md) | 简体中文
--- ## 🚩 项目简介 完全开源免费的自托管 SSL 证书 ACME 工具,申请、部署、续期、监控全流程自动化可视化,支持各大主流云厂商。 - **自托管**:私有化部署,所有数据本地化存储,掌控数据的隐私与安全。 - **零依赖**:无需安装数据库、运行时或复杂框架,一键启动,开箱即用。 - **低占用**:超轻量的资源开销,仅需 ~16 MB 内存,甚至可以运行在家用路由器。 - **易操作**:图形化界面,通过简单配置即可完成证书申请、部署和续期的自动化工作。 ## 💡 功能特性 - 灵活的工作流编排方式,证书从申请到部署完全自动化。 - 支持申请单/多/泛域名证书、IP 地址证书,可选 RSA、ECC 私钥算法。 - 支持 DNS-01(即基于域名解析验证)、HTTP-01(即基于文件验证)两种质询方式。 - 支持 PEM、PFX、JKS 等多种格式输出证书。 - 支持 60+ 域名托管商(如阿里云、腾讯云、AWS、Cloudflare、GoDaddy 等,[点此查看完整清单](https://docs.certimate.me/zh-CN/docs/reference/providers#supported-dns-providers))。 - 支持 110+ 部署目标(如 Kubernetes、CDN、WAF、负载均衡等,[点此查看完整清单](https://docs.certimate.me/zh-CN/docs/reference/providers#supported-hosting-providers))。 - 支持邮件、钉钉、飞书、企业微信、Discord、Slack、Telegram 等多种通知渠道。 - 支持 Let's Encrypt、Actalis、Google Trust Services、SSL.com、ZeroSSL 等多种 ACME 证书颁发机构。 - 更多特性等待探索。 ## 🚀 快速启动 **1 分钟运行 Certimate!**
👉 二进制安装: 从 [GitHub Releases](https://github.com/certimate-go/certimate/releases) 页面下载预先编译好的可执行文件压缩包,解压缩后在终端中执行: ```bash ./certimate serve ```
👉 Docker 安装: ```bash docker run -d \ --name certimate \ --restart unless-stopped \ -p 8090:8090 \ -v /etc/localtime:/etc/localtime:ro \ -v /etc/timezone:/etc/timezone:ro \ -v $(pwd)/data:/app/pb_data \ certimate/certimate:latest ```
浏览器中访问 `http://127.0.0.1:8090`。 初始的管理员账号及密码: - 账号:`admin@certimate.fun` - 密码:`1234567890` 即刻使用 Certimate。或者阅读文档中的其他内容以了解更多。 ## 📄 使用手册 请访问文档站 [docs.certimate.me](https://docs.certimate.me/) 以阅读使用手册。 > (由于众所周知的原因,中国大陆用户可能需要 🪄 上网才能访问文档站。) 相关文章: > - [《升级指南:迁移到 v0.4》](https://docs.certimate.me/zh-CN/docs/migrations/migrate-to-v0.4) > - [《使用 CNAME 完成 ACME DNS-01 质询》](https://docs.certimate.me/zh-CN/blog/cname) > - [《Why Certimate?》](https://docs.certimate.me/zh-CN/blog/why-certimate) ## 🖥️ 运行界面 [![Screenshot](https://i.imgur.com/4DAUKEE.gif)](https://www.bilibili.com/video/BV1xockeZEm2) ## 🤝 参与贡献 Certimate 是一个免费且开源的项目。我们欢迎任何人为 Certimate 做出贡献,以帮助改善 Certimate。包括但不限于:提交代码、反馈缺陷、交流想法,或分享你基于 Certimate 的使用案例。同时,我们也欢迎用户在个人博客或社交媒体上分享 Certimate。 如果你想要贡献代码,请先阅读我们的[贡献指南](./CONTRIBUTING.md)。 请在 https://github.com/certimate-go/certimate 提交 [Issues](https://github.com/certimate-go/certimate/issues) 和 [Pull Requests](https://github.com/certimate-go/certimate/pulls)。 #### 感谢以下贡献者对 Certimate 做出的贡献: [![Contributors](https://contrib.rocks/image?repo=certimate-go/certimate)](https://github.com/certimate-go/certimate/graphs/contributors) ## ⛔ 免责声明 Certimate 遵循 [MIT License](https://opensource.org/licenses/MIT) 开源协议,完全免费提供,旨在“按现状”供用户使用。作者及贡献者不对使用本软件所产生的任何直接或间接后果承担责任,包括但不限于性能下降、数据丢失、服务中断、或任何其他类型的损害。 **无任何保证**:本软件不提供任何明示或暗示的保证,包括但不限于对特定用途的适用性、无侵权性、商用性及可靠性的保证。 **用户责任**:使用本软件即表示您理解并同意承担由此产生的一切风险及责任。 ## 🌐 加入社群 - [Telegram](https://t.me/+ZXphsppxUg41YmVl) - 微信群聊(因微信自身限制需群主邀请,可先加 [@usual2970](https://github.com/usual2970) 好友) ## ⭐ 星标趋势 在 GitHub 上为 Certimate 添加 Star 星标关注,即可第一时间获取新版本发布通知。 [![Stargazers over time](https://starchart.cc/certimate-go/certimate.svg?variant=adaptive)](https://starchart.cc/certimate-go/certimate) ================================================ FILE: cmd/intercmd.go ================================================ package cmd import ( "context" "errors" "fmt" "log" "os" "github.com/go-acme/lego/v4/lego" legolog "github.com/go-acme/lego/v4/log" "github.com/pocketbase/pocketbase/core" "github.com/spf13/cobra" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/internal/certacme" "github.com/certimate-go/certimate/internal/tools/mproc" ) func NewInternalCommand(app core.App) *cobra.Command { command := &cobra.Command{ Use: "intercmd", Short: "[INTERNAL] Internal dedicated for Certimate", } command.AddCommand(internalCertApplyCommand(app)) return command } func internalCertApplyCommand(_ core.App) *cobra.Command { var flagInput string var flagOutput string var flagError string var flagEncryptionKey string command := &cobra.Command{ Use: "certapply", Example: "internal certapply --in ./in.file --out ./out.file --enckey aeskey", SilenceUsage: true, Run: func(cmd *cobra.Command, args []string) { type InData struct { Account *certacme.ACMEAccount `json:"account,omitempty"` Request *certacme.ObtainCertificateRequest `json:"request,omitempty"` } type OutData struct { Response *certacme.ObtainCertificateResponse `json:"response"` } mreceiver := mproc.NewReceiver(func(ctx context.Context, params *InData) (*OutData, error) { if params.Account == nil { return nil, errors.New("illegal params") } if params.Request == nil { return nil, errors.New("illegal params") } // redirect to stdout, remove datetime prefix // so that the logger can split logs correctly // see: /internal/tools/mproc/sender.go legolog.Logger = log.New(os.Stdout, "", 0) client, err := certacme.NewACMEClientWithAccount(params.Account, func(c *lego.Config) error { c.UserAgent = app.AppUserAgent c.Certificate.KeyType = params.Request.PrivateKeyType c.Certificate.DisableCommonName = params.Request.NoCommonName return nil }) if err != nil { return nil, fmt.Errorf("failed to initialize acme client: %w", err) } resp, err := client.ObtainCertificate(ctx, params.Request) if err != nil { return nil, fmt.Errorf("failed to obtain certificate: %w", err) } return &OutData{ Response: resp, }, nil }) if err := mreceiver.ReceiveWithContext(cmd.Context(), flagInput, flagOutput, flagEncryptionKey); err != nil { os.WriteFile(flagError, []byte(err.Error()), 0o644) } }, } command.PersistentFlags().StringVar(&flagInput, "in", "", "") command.PersistentFlags().StringVar(&flagOutput, "out", "", "") command.PersistentFlags().StringVar(&flagError, "err", "", "") command.PersistentFlags().StringVar(&flagEncryptionKey, "enckey", "", "") return command } ================================================ FILE: cmd/serve_nonwindows.go ================================================ //go:build !windows // +build !windows package cmd import ( "github.com/pocketbase/pocketbase" ) func Serve(app *pocketbase.PocketBase) error { return app.Start() } ================================================ FILE: cmd/serve_windows.go ================================================ //go:build windows // +build windows package cmd import ( "fmt" "github.com/pocketbase/pocketbase" "github.com/pocketbase/pocketbase/core" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/eventlog" ) type winscHandler struct { pb *pocketbase.PocketBase elog *eventlog.Log } func (h *winscHandler) Execute(args []string, r <-chan svc.ChangeRequest, s chan<- svc.Status) (bool, uint32) { go func() { if err := h.pb.Start(); err != nil { h.elog.Error(999, fmt.Sprintf("Start failed: %v", err)) } }() s <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown} for { select { case c := <-r: switch c.Cmd { case svc.Interrogate: s <- c.CurrentStatus case svc.Stop, svc.Shutdown: event := new(core.TerminateEvent) event.App = h.pb h.pb.OnTerminate().Trigger(event, func(e *core.TerminateEvent) error { return e.App.ResetBootstrapState() }) s <- svc.Status{State: svc.Stopped} return false, 0 default: h.elog.Warning(998, fmt.Sprintf("unexpected control request: %v", c.Cmd)) } } } } func Serve(app *pocketbase.PocketBase) error { if isWinsc, _ := svc.IsWindowsService(); isWinsc { elog, _ := eventlog.Open(winscName) defer elog.Close() return svc.Run(winscName, &winscHandler{pb: app, elog: elog}) } return app.Start() } ================================================ FILE: cmd/winsc_nonwindows.go ================================================ //go:build !windows // +build !windows package cmd import ( "github.com/pocketbase/pocketbase/core" "github.com/spf13/cobra" ) func NewWinscCommand(app core.App) *cobra.Command { command := &cobra.Command{ Use: "winsc", Short: "Install/Uninstall Windows service (Not supported on non-Windows OS)", } return command } ================================================ FILE: cmd/winsc_windows.go ================================================ //go:build windows // +build windows package cmd import ( "fmt" "log/slog" "os" "time" "github.com/pocketbase/pocketbase/core" "github.com/spf13/cobra" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/eventlog" "golang.org/x/sys/windows/svc/mgr" "github.com/certimate-go/certimate/internal/app" ) const winscName = "certimate" func NewWinscCommand(app core.App) *cobra.Command { command := &cobra.Command{ Use: "winsc", Short: "Install/Uninstall Windows service", } command.AddCommand(winscInstallCommand(app)) command.AddCommand(winscUninstallCommand(app)) command.AddCommand(winscStartCommand(app)) command.AddCommand(winscStopCommand(app)) return command } func winscInstallCommand(_ core.App) *cobra.Command { command := &cobra.Command{ Use: "install [args...]", Example: "winsc install", Run: func(cmd *cobra.Command, args []string) { srvPath, err := os.Executable() if err != nil { srvPath = os.Args[0] } srvArgs := []string{"serve"} srvArgs = append(srvArgs, args...) manager, err := mgr.Connect() if err != nil { slog.Error(fmt.Sprintf("failed to connect to service manager: %v", err)) return } defer manager.Disconnect() config := mgr.Config{ DisplayName: app.AppName, Description: "https://github.com/certimate-go/certimate", StartType: mgr.StartAutomatic, } service, err := manager.CreateService(winscName, srvPath, config, srvArgs...) if err != nil { slog.Error(fmt.Sprintf("failed to create service: %v", err)) return } defer service.Close() eventlog.InstallAsEventCreate(winscName, eventlog.Error|eventlog.Warning|eventlog.Info) slog.Info(fmt.Sprintf("service '%s' installed", winscName)) if err := service.Start(); err != nil { slog.Warn(fmt.Sprintf("failed to start service: %v", err)) } slog.Info(fmt.Sprintf("service '%s' started", winscName)) }, DisableFlagParsing: true, } return command } func winscUninstallCommand(_ core.App) *cobra.Command { command := &cobra.Command{ Use: "uninstall", Example: "winsc uninstall", Run: func(cmd *cobra.Command, args []string) { manager, err := mgr.Connect() if err != nil { slog.Error(fmt.Sprintf("failed to connect to service manager: %v", err)) return } defer manager.Disconnect() service, err := manager.OpenService(winscName) if err != nil { slog.Error(fmt.Sprintf("failed to open service: %v", err)) return } defer service.Close() status, err := service.Query() if err == nil && status.State != svc.Stopped { _, err = service.Control(svc.Stop) if err != nil { slog.Warn(fmt.Sprintf("failed to stop service: %v", err)) } time.Sleep(3 * time.Second) slog.Info(fmt.Sprintf("service '%s' stopped", winscName)) } if err = service.Delete(); err != nil { slog.Error(fmt.Sprintf("failed to delete service: %v", err)) return } eventlog.Remove(winscName) slog.Info(fmt.Sprintf("service '%s' uninstalled", winscName)) }, } return command } func winscStartCommand(_ core.App) *cobra.Command { command := &cobra.Command{ Use: "start", Example: "winsc start", Run: func(cmd *cobra.Command, args []string) { manager, err := mgr.Connect() if err != nil { slog.Error(fmt.Sprintf("failed to connect to service manager: %v", err)) return } defer manager.Disconnect() service, err := manager.OpenService(winscName) if err != nil { slog.Error(fmt.Sprintf("failed to open service: %v", err)) return } defer service.Close() if err := service.Start(); err != nil { slog.Error(fmt.Sprintf("failed to start service: %v", err)) return } slog.Info(fmt.Sprintf("service '%s' started", winscName)) }, } return command } func winscStopCommand(app core.App) *cobra.Command { command := &cobra.Command{ Use: "stop", Example: "winsc stop", Run: func(cmd *cobra.Command, args []string) { manager, err := mgr.Connect() if err != nil { slog.Error(fmt.Sprintf("failed to connect to service manager: %v", err)) return } defer manager.Disconnect() service, err := manager.OpenService(winscName) if err != nil { slog.Error(fmt.Sprintf("failed to open service: %v", err)) return } defer service.Close() status, err := service.Query() if err == nil && status.State != svc.Stopped { _, err = service.Control(svc.Stop) if err != nil { slog.Warn(fmt.Sprintf("failed to stop service: %v", err)) } time.Sleep(3 * time.Second) slog.Info(fmt.Sprintf("service '%s' stopped", winscName)) } }, } return command } ================================================ FILE: docker/docker-compose.yml ================================================ version: "3.0" services: certimate: image: certimate/certimate:latest container_name: certimate ports: - 8090:8090 volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - ./data:/app/pb_data restart: unless-stopped ================================================ FILE: go.mod ================================================ module github.com/certimate-go/certimate go 1.25.0 toolchain go1.25.5 require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.4.0 github.com/G-Core/gcorelabscdn-go v1.0.35 github.com/KscSDK/ksc-sdk-go v0.18.0 github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 github.com/alibabacloud-go/alb-20200616/v2 v2.3.1 github.com/alibabacloud-go/apig-20240327/v6 v6.0.1 github.com/alibabacloud-go/cas-20200407/v4 v4.1.0 github.com/alibabacloud-go/cdn-20180510/v9 v9.0.0 github.com/alibabacloud-go/cloudapi-20160714/v5 v5.7.9 github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15 github.com/alibabacloud-go/dcdn-20180115/v4 v4.1.0 github.com/alibabacloud-go/ddoscoo-20200101/v5 v5.0.1 github.com/alibabacloud-go/esa-20240910/v2 v2.48.0 github.com/alibabacloud-go/fc-20230330/v4 v4.6.8 github.com/alibabacloud-go/fc-open-20210406/v2 v2.0.12 github.com/alibabacloud-go/ga-20191120/v4 v4.0.0 github.com/alibabacloud-go/live-20161101/v2 v2.6.0 github.com/alibabacloud-go/nlb-20220430/v4 v4.1.2 github.com/alibabacloud-go/openapi-util v0.1.1 github.com/alibabacloud-go/slb-20140515/v4 v4.0.13 github.com/alibabacloud-go/tea v1.4.0 github.com/alibabacloud-go/tea-utils/v2 v2.0.9 github.com/alibabacloud-go/vod-20170321/v4 v4.11.1 github.com/alibabacloud-go/waf-openapi-20211001/v7 v7.5.0 github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.4.0 github.com/aws/aws-sdk-go-v2 v1.41.2 github.com/aws/aws-sdk-go-v2/config v1.32.10 github.com/aws/aws-sdk-go-v2/credentials v1.19.10 github.com/aws/aws-sdk-go-v2/service/acm v1.37.20 github.com/aws/aws-sdk-go-v2/service/cloudfront v1.60.1 github.com/aws/aws-sdk-go-v2/service/iam v1.53.3 github.com/baidubce/bce-sdk-go v0.9.260 github.com/byteplus-sdk/byteplus-sdk-golang v1.0.62 github.com/go-acme/lego/v4 v4.32.0 github.com/go-cmd/cmd v1.4.3 github.com/go-resty/resty/v2 v2.17.1 github.com/go-viper/mapstructure/v2 v2.5.0 github.com/google/go-querystring v1.2.0 github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187 github.com/jdcloud-api/jdcloud-sdk-go v1.64.0 github.com/kong/go-kong v0.72.1 github.com/luthermonson/go-proxmox v0.3.2 github.com/microcosm-cc/bluemonday v1.0.27 github.com/minio/minio-go/v7 v7.0.98 github.com/mohuatech/mohuacloud-go-sdk v0.0.0-20251115182757-6fba4d0a4c47 github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0 github.com/pkg/sftp v1.13.10 github.com/pocketbase/dbx v1.12.0 github.com/pocketbase/pocketbase v0.36.5 github.com/povsister/scp v0.0.0-20250701154629-777cf82de5df github.com/pquerna/otp v1.5.0 github.com/qiniu/go-sdk/v7 v7.25.6 github.com/samber/lo v1.52.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.3.36 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.3.45 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap v1.3.34 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/live v1.3.45 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/scf v1.3.29 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.3.42 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.3.45 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vod v1.3.46 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/waf v1.3.46 github.com/ucloud/ucloud-sdk-go v0.22.59 github.com/volcengine/ve-tos-golang-sdk/v2 v2.9.1 github.com/volcengine/volc-sdk-golang v1.0.237 github.com/volcengine/volcengine-go-sdk v1.2.15 github.com/wneessen/go-mail v0.7.2 github.com/xhit/go-str2duration/v2 v2.1.0 gitlab.ecloud.com/ecloud/ecloudsdkclouddns v1.0.1 gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0 golang.org/x/crypto v0.48.0 golang.org/x/sync v0.19.0 golang.org/x/sys v0.41.0 k8s.io/api v0.35.2 k8s.io/apimachinery v0.35.2 k8s.io/client-go v0.35.2 software.sslmate.com/src/go-pkcs12 v0.7.0 ) require ( github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect github.com/alibabacloud-go/alibabacloud-gateway-fc-util v0.0.7 // indirect github.com/avast/retry-go v3.0.0+incompatible // indirect github.com/aws/aws-sdk-go v1.40.45 // indirect github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/buger/goterm v1.0.4 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/diskfs/go-diskfs v1.7.0 // indirect github.com/djherbis/times v1.6.0 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-acme/alidns-20150109/v4 v4.7.0 // indirect github.com/go-acme/tencentclouddnspod v1.3.24 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.23.0 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/goccy/go-yaml v1.9.8 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/jinzhu/copier v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kong/semver/v4 v4.0.1 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/linode/linodego v1.65.0 // indirect github.com/magefile/mage v1.15.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/namedotcom/go/v4 v4.0.2 // indirect github.com/nrdcg/bunny-go v0.1.0 // indirect github.com/nrdcg/desec v0.11.1 // indirect github.com/nrdcg/goacmedns v0.2.0 // indirect github.com/nrdcg/porkbun v0.4.0 // indirect github.com/ovh/go-ovh v1.9.0 // indirect github.com/peterhellberg/link v1.2.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/qiniu/dyn v1.3.0 // indirect github.com/qiniu/x v1.10.5 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/vultr/govultr/v3 v3.27.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.mongodb.org/mongo-driver v1.17.2 // indirect go.uber.org/ratelimit v0.3.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ns1/ns1-go.v2 v2.17.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect github.com/alibabacloud-go/debug v1.0.1 // indirect github.com/alibabacloud-go/endpoint-util v1.1.1 // indirect github.com/aliyun/credentials-go v1.4.7 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect github.com/aws/smithy-go v1.24.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/disintegration/imaging v1.6.2 // indirect github.com/domodwyer/mailyak/v3 v3.6.2 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fatih/color v1.18.0 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/ganigeorgiev/fexpr v0.5.0 // indirect github.com/go-acme/esa-20240910/v2 v2.48.0 // indirect github.com/go-acme/jdcloud-sdk-go v1.64.0 // indirect github.com/go-acme/tencentedgdeone v1.3.38 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/klauspost/cpuid/v2 v2.2.11 // indirect github.com/klauspost/crc32 v1.3.0 // 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/miekg/dns v1.1.72 // indirect github.com/minio/crc64nvme v1.1.1 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/nrdcg/namesilo v0.5.0 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/rs/xid v1.6.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/tinylib/msgp v1.6.1 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect golang.org/x/image v0.36.0 // indirect golang.org/x/mod v0.32.0 // indirect golang.org/x/net v0.50.0 // indirect golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/term v0.40.0 // indirect golang.org/x/text v0.34.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.41.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/ini.v1 v1.67.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.67.6 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect modernc.org/sqlite v1.46.1 // indirect ) replace gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0 => ./pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkcore@v1.0.0 replace gitlab.ecloud.com/ecloud/ecloudsdkclouddns v1.0.1 => ./pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1 ================================================ FILE: go.sum ================================================ 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= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 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/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 h1:yzrctSl9GMIQ5lHu7jc8olOsGjWDCsBpJhWqfGa/YIM= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0/go.mod h1:GE4m0rnnfwLGX0Y9A9A25Zx5N/90jneT5ABevqzhuFQ= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 h1:zLzoX5+W2l95UJoVwiyNS4dX8vHyQ6x2xRLoBBL9wMk= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.4.0 h1:mtvR5ZXH5Ew6PSONd5lO5OXovWP1E3oAlgC8fpxor2Q= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.4.0/go.mod h1:u560+RFVfG0CBPzkXlDW43slESbBAQjgDGi3r6z+wk8= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/G-Core/gcorelabscdn-go v1.0.35 h1:7UFoL1jSb8e+JN1xxQisGE8gtflqx1vM1gH7wa9fa1E= github.com/G-Core/gcorelabscdn-go v1.0.35/go.mod h1:iSGXaTvZBzDHQW+rKFS918BgFVpONcyLEijwh8WsXpE= 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/KscSDK/ksc-sdk-go v0.18.0 h1:Lix27hvZ9K4WTj4qUwh+2fbXYuMp9jBpVbnnmeiCg5U= github.com/KscSDK/ksc-sdk-go v0.18.0/go.mod h1:isHlJZi429ff5JLemSc10h7nznNgzJAY4MmNM8u7SBo= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 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= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae/go.mod h1:/cvHQkZ1fst0EmZnA5dFtiQdWCNCFYzb+uE2vqVgvx0= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 h1:h/33OxYLqBk0BYmEbSUy7MlvgQR/m1w1/7OJFKoPL1I= github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0/go.mod h1:rvh3imDA6EaQi+oM/GQHkQAOHbXPKJ7EWJvfjuw141Q= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI= github.com/alibabacloud-go/alb-20200616/v2 v2.3.1 h1:IYWpYnBpRUY35vA0/Qxedqwkl2oMlwFf7UhibbUXkEE= github.com/alibabacloud-go/alb-20200616/v2 v2.3.1/go.mod h1:pUTnSOSknoHg5YtAmGrXuO+JcPlb+EYRNutf5VbW/F0= github.com/alibabacloud-go/alibabacloud-gateway-fc-util v0.0.7 h1:RDatRb9RG39HjkevgzTeiVoDDaamoB+12GHNairp3Ag= github.com/alibabacloud-go/alibabacloud-gateway-fc-util v0.0.7/go.mod h1:H0RPHXHP/ICfEQrKzQcCqXI15jcV4zaDPCOAmh3U9O8= github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA= github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo= github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8= github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g= github.com/alibabacloud-go/apig-20240327/v6 v6.0.1 h1:LneGoh/nC1Dv39qGOrXH5py2D6kFHSkr1etNuo2dxls= github.com/alibabacloud-go/apig-20240327/v6 v6.0.1/go.mod h1:VCQaugCTmRp5E1HXWFnCdpJP+UVSFkaJBn787UpR6Qw= github.com/alibabacloud-go/cas-20200407/v4 v4.1.0 h1:JldJ1EtKHzqZMQJkZaGKz4pI6TtbKCKTXNO/v2bVJ30= github.com/alibabacloud-go/cas-20200407/v4 v4.1.0/go.mod h1:q7X8C3NE71dRxR3YLwz/NESvE5X56RI2tGTJqODe7Zs= github.com/alibabacloud-go/cdn-20180510/v9 v9.0.0 h1:HNutnXWhtfPUjlUEOfMvzqVXpQip11eqK4vSMM0o+UA= github.com/alibabacloud-go/cdn-20180510/v9 v9.0.0/go.mod h1:6UcbZ0B2z0B1mnquRrsB0vCKwNcgBJE70y3PIn3y0Eo= github.com/alibabacloud-go/cloudapi-20160714/v5 v5.7.9 h1:lFUrf4dvUmbTkAW56fyKdNauSStUpNR4i7cFWWKu/pY= github.com/alibabacloud-go/cloudapi-20160714/v5 v5.7.9/go.mod h1:kPth3SgnjK42No8O5biqjrAeDgMd/cFGUktq8g9Vs4A= github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY= github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI= github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE= github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8= github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc= github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc= github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.5/go.mod h1:kUe8JqFmoVU7lfBauaDD5taFaW7mBI+xVsyHutYtabg= github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE= github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.14/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE= github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15 h1:Mubp9hXZMTPWZK+WxrR+kKOVFp4Q/PDZrIIM7ByXI9Y= github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.15/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE= github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg= github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ= github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo= github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA= github.com/alibabacloud-go/dcdn-20180115/v4 v4.1.0 h1:3Q7qvpL2+k+7Twda0VE0MC0vfoRAxCtOl36S7vDLmjY= github.com/alibabacloud-go/dcdn-20180115/v4 v4.1.0/go.mod h1:dVyxkadBhESK7HlppUEjdaJmw6e5ZlZNwy8+BTSDcRE= github.com/alibabacloud-go/ddoscoo-20200101/v5 v5.0.1 h1:vEwgCBuQxrTaThLC4eMOko/XjAPT9WIg0t0gk+ABJiE= github.com/alibabacloud-go/ddoscoo-20200101/v5 v5.0.1/go.mod h1:oYNvOuLR67SMppvBmB9Hb9jnJFDQtLLEN/Rbukbq0w0= github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY= github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc= github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg= github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc= github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE= github.com/alibabacloud-go/endpoint-util v1.1.1 h1:ZkBv2/jnghxtU0p+upSU0GGzW1VL9GQdZO3mcSUTUy8= github.com/alibabacloud-go/endpoint-util v1.1.1/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE= github.com/alibabacloud-go/esa-20240910/v2 v2.48.0 h1:SdtLjxay5rzshlp56bvHFSqWuKwi+rkhCfla4cRuDVU= github.com/alibabacloud-go/esa-20240910/v2 v2.48.0/go.mod h1:uSzaHIUBmr4WoixyRnc8uEuzSqxy/HQ4F8iu4RAzvHQ= github.com/alibabacloud-go/fc-20230330/v4 v4.6.8 h1:nM/hqf/9ERwN24z00kE66TQfq2NmaCUzFPUnGrwZGdY= github.com/alibabacloud-go/fc-20230330/v4 v4.6.8/go.mod h1:EQNGiZWcKvBqs6rHHyAtWau1qeTR5A/yiuUI84b7NdA= github.com/alibabacloud-go/fc-open-20210406/v2 v2.0.12 h1:A3D8Mp6qf8DfR6Dt5MpS8aDVaWfS4N85T5CvGUvgrjM= github.com/alibabacloud-go/fc-open-20210406/v2 v2.0.12/go.mod h1:F5c0E5UB3k8v6neTtw3FBcJ1YCNFzVoL1JPRHTe33u4= github.com/alibabacloud-go/ga-20191120/v4 v4.0.0 h1:bigMbQy6TXKMwhsRHqqjo+6dQcv0SZ+nzfxd8N2D7SE= github.com/alibabacloud-go/ga-20191120/v4 v4.0.0/go.mod h1:07e+SHN7j6s6hL/dSK6TZHIqvWBc0tbWW/iW5BPjM2Q= github.com/alibabacloud-go/live-20161101/v2 v2.6.0 h1:wi9/Mi5CDYeXquB39B8Ch0/CtuCDoSuQxDw1bY+dl0U= github.com/alibabacloud-go/live-20161101/v2 v2.6.0/go.mod h1:1BN//Z4vOkdEplf0pWcpF1GuIqaPJOwYuPCShljY+nI= github.com/alibabacloud-go/nlb-20220430/v4 v4.1.2 h1:dKDAynkCI9qGAlZIaNNGbiLTCLhD5yzqjlQHdbe0lNQ= github.com/alibabacloud-go/nlb-20220430/v4 v4.1.2/go.mod h1:jYaLW+5IteqlZ8becBP51zPQp42PxjMHLbqMpU5Cyds= github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= github.com/alibabacloud-go/openapi-util v0.1.1 h1:ujGErJjG8ncRW6XtBBMphzHTvCxn4DjrVw4m04HsS28= github.com/alibabacloud-go/openapi-util v0.1.1/go.mod h1:/UehBSE2cf1gYT43GV4E+RxTdLRzURImCYY0aRmlXpw= github.com/alibabacloud-go/slb-20140515/v4 v4.0.13 h1:MtQUoGTgFqGTebY4lzFTFVsIV7QXeVN13oMzJYqvtYQ= github.com/alibabacloud-go/slb-20140515/v4 v4.0.13/go.mod h1:gWZrz3AD+izASfHjpxTOIJ8N0KMRjbIRzRZr1koy7tA= github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg= github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= github.com/alibabacloud-go/tea v1.2.1/go.mod h1:qbzof29bM/IFhLMtJPrgTGK3eauV5J2wSyEUo4OEmnA= github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk= github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg= github.com/alibabacloud-go/tea v1.4.0 h1:MSKhu/kWLPX7mplWMngki8nNt+CyUZ+kfkzaR5VpMhA= github.com/alibabacloud-go/tea v1.4.0/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg= github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= github.com/alibabacloud-go/tea-utils/v2 v2.0.4/go.mod h1:sj1PbjPodAVTqGTA3olprfeeqqmwD0A5OQz94o9EuXQ= github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4= github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= github.com/alibabacloud-go/tea-utils/v2 v2.0.9 h1:y6pUIlhjxbZl9ObDAcmA1H3c21eaAxADHTDQmBnAIgA= github.com/alibabacloud-go/tea-utils/v2 v2.0.9/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8= github.com/alibabacloud-go/vod-20170321/v4 v4.11.1 h1:EPenvECObhGH01jaChRb7NRzNrk7eU3iFyZBksQS+zc= github.com/alibabacloud-go/vod-20170321/v4 v4.11.1/go.mod h1:2NX/9lVaKpd1+1GEV5zUAzQFfK9pF8Wkx81ugAnHYiw= github.com/alibabacloud-go/waf-openapi-20211001/v7 v7.5.0 h1:QYKzVRu0C/stONFvxnwbYUbpSSauMQrdReekQw4ULqk= github.com/alibabacloud-go/waf-openapi-20211001/v7 v7.5.0/go.mod h1:g+049bOg+Vh40ckFRzg5kCc7r3kJxYi5aqH/uuRZ+qA= github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.4.0 h1:gfxyMc5g9TJ4TO/PQ8PvkGfYpDUHZnVGP0/7iTgI0Ks= github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.4.0/go.mod h1:FTzydeQVmR24FI0D6XWUOMKckjXehM/jgMn1xC+DA9M= github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw= github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0= github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM= github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= github.com/aliyun/credentials-go v1.4.7 h1:T17dLqEtPUFvjDRRb5giVvLh6dFT8IcNFJJb7MeyCxw= github.com/aliyun/credentials-go v1.4.7/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs= github.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/aws/aws-sdk-go v1.25.3/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.40.45 h1:QN1nsY27ssD/JmW4s83qmSb+uL6DG4GmCDzjmJB4xUI= github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= 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.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls= github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4= github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI= github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw= github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8= github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/service/acm v1.37.20 h1:lK39/l75lJkopS7WIk8bhGnWstTOfFVYtozVW8uoqlM= github.com/aws/aws-sdk-go-v2/service/acm v1.37.20/go.mod h1:3iaG4YcV+H0ERcefngFFs+ZpFfUaUY8Q0GA8TmkDtE8= github.com/aws/aws-sdk-go-v2/service/cloudfront v1.60.1 h1:fwkGr0AyYMq/oxzBrNWVLcmSgSWVyGtFAanNs+ECRes= github.com/aws/aws-sdk-go-v2/service/cloudfront v1.60.1/go.mod h1:PAegJVxp+CkgKZBZVEaTWBN2bHwH24FLl5sIIHYuzOU= 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/iam v1.53.3 h1:boKZv8dNdHznhAA68hb/dqFz5pxoWmRAOJr9LtscVCI= github.com/aws/aws-sdk-go-v2/service/iam v1.53.3/go.mod h1:E0QHh3aEwxYb7xshjvxYDELiOda7KBYJ77e/TvGhpcM= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk= github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 h1:1jIdwWOulae7bBLIgB36OZ0DINACb1wxM6wdGlx4eHE= github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1/go.mod h1:tE2zGlMIlxWv+7Otap7ctRp3qeKqtnja7DZguj3Vu/Y= github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ= github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g= github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o= github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU= github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c= github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs= github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0= github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/baidubce/bce-sdk-go v0.9.260 h1:1v1+2GTP+NGK3L24rJ+bnoiTaDaIy2YoaUM+ot2GTcw= github.com/baidubce/bce-sdk-go v0.9.260/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY= github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE= github.com/byteplus-sdk/byteplus-sdk-golang v1.0.62 h1:36+wcU891+eaanXqlBSacckSyHmyy11iSFoEFVS6x/8= github.com/byteplus-sdk/byteplus-sdk-golang v1.0.62/go.mod h1:CIL/T2dxgbIA79os+wl0Fq0vCbADTZNIddV6PNYB6DY= github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= 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/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= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 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.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/diskfs/go-diskfs v1.7.0 h1:vonWmt5CMowXwUc79jWyGrf2DIMeoOjkLlMnQYGVOs8= github.com/diskfs/go-diskfs v1.7.0/go.mod h1:LhQyXqOugWFRahYUSw47NyZJPezFzB9UELwhpszLP/k= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8= github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY= github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gammazero/toposort v0.1.1/go.mod h1:H2cozTnNpMw0hg2VHAYsAxmkHXBYroNangj2NTBQDvw= github.com/ganigeorgiev/fexpr v0.5.0 h1:XA9JxtTE/Xm+g/JFI6RfZEHSiQlk+1glLvRK1Lpv/Tk= github.com/ganigeorgiev/fexpr v0.5.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-acme/alidns-20150109/v4 v4.7.0 h1:PqJ/wR0JTpL4v0Owu1uM7bPQ1Yww0eQLAuuSdLjjQaQ= github.com/go-acme/alidns-20150109/v4 v4.7.0/go.mod h1:btQvB6xZoN6ykKB74cPhiR+uvhrEE2AFVXm6RDmCHm0= github.com/go-acme/esa-20240910/v2 v2.48.0 h1:muSDyhjDTejxUGe3FTthCPCqRaEdYY9cG3N/AmU52Lc= github.com/go-acme/esa-20240910/v2 v2.48.0/go.mod h1:shPb6hzc1rJL15IJBY8HQ4GZk4E8RC52+52twutEwIg= github.com/go-acme/jdcloud-sdk-go v1.64.0 h1:AW9j5khk8tRYbpBJPxKmqdwIqgLs2Fz3HUK3hn2YXjs= github.com/go-acme/jdcloud-sdk-go v1.64.0/go.mod h1:qc/m8HNX1Zgd7GAv2DSEinup8fwy3Ted3/VVx7LB5bU= github.com/go-acme/lego/v4 v4.32.0 h1:z7Ss7aa1noabhKj+DBzhNCO2SM96xhE3b0ucVW3x8Tc= github.com/go-acme/lego/v4 v4.32.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI= github.com/go-acme/tencentclouddnspod v1.3.24 h1:uCSiOW1EJttcnOON+MVVyVDJguFL/Q4NIGkq1CrT9p8= github.com/go-acme/tencentclouddnspod v1.3.24/go.mod h1:RKcB2wSoZncjBA0OEFj59s1ko1XDy+ZsAtk+9uMxUF0= github.com/go-acme/tencentedgdeone v1.3.38 h1:5YsVl0H4A+cwtiUqR1eZbKFdr4OWfYp2KYJopifzKyQ= github.com/go-acme/tencentedgdeone v1.3.38/go.mod h1:yyjTKVmGpMtFv5HqGODqehHnZJ4KWAbG6dAiwWDgCDY= github.com/go-cmd/cmd v1.4.3 h1:6y3G+3UqPerXvPcXvj+5QNPHT02BUw7p6PsqRxLNA7Y= github.com/go-cmd/cmd v1.4.3/go.mod h1:u3hxg/ry+D5kwh8WvUkHLAMe2zQCaXd00t35WfQaOFk= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es= github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.7.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk= github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4= github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= github.com/goccy/go-yaml v1.9.8 h1:5gMyLUeU1/6zl+WFfR1hN7D2kf+1/eRGa7DFtToiBvQ= github.com/goccy/go-yaml v1.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187 h1:J+U6+eUjIsBhefolFdZW5hQNJbkMj+7msxZrv56Cg2g= github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI= github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A= github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jdcloud-api/jdcloud-sdk-go v1.64.0 h1:xZc/ZRcrOhDx9Ra9htu6ui2gUUttmLsXIqH61LcvY4U= github.com/jdcloud-api/jdcloud-sdk-go v1.64.0/go.mod h1:UrKjuULIWLjHFlG6aSPunArE5QX57LftMmStAZJBEX8= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU= github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 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/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU= github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= github.com/kong/go-kong v0.72.1 h1:rQ69f3Wd0Fvc3JANkavo34vePqR4uZG/YQ2y5U7d2Po= github.com/kong/go-kong v0.72.1/go.mod h1:J0vGB3wsZ2i99zly1zTRe3v7rOKpkhQZRwbcTFP76qM= github.com/kong/semver/v4 v4.0.1 h1:DIcNR8W3gfx0KabFBADPalxxsp+q/5COwIFkkhrFQ2Y= github.com/kong/semver/v4 v4.0.1/go.mod h1:LImQ0oT15pJvSns/hs2laLca2zcYoHu5EsSNY0J6/QA= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7k= github.com/linode/linodego v1.65.0/go.mod h1:tOFiTErdjkbVnV+4S0+NmIE9dqqZUEM2HsJaGu8wMh8= github.com/luthermonson/go-proxmox v0.3.2 h1:/zUg6FCl9cAABx0xU3OIgtDtClY0gVXxOCsrceDNylc= github.com/luthermonson/go-proxmox v0.3.2/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ= github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= 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.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= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 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/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 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= github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI= github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.98 h1:MeAVKjLVz+XJ28zFcuYyImNSAh8Mq725uNW4beRisi0= github.com/minio/minio-go/v7 v7.0.98/go.mod h1:cY0Y+W7yozf0mdIclrttzo1Iiu7mEf9y7nk2uXqMOvM= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohuatech/mohuacloud-go-sdk v0.0.0-20251115182757-6fba4d0a4c47 h1:ymaxpfg8BH3Jlecq943X/+QWOBuMp1qmRUCK+SCoN+c= github.com/mohuatech/mohuacloud-go-sdk v0.0.0-20251115182757-6fba4d0a4c47/go.mod h1:+GS72hJwcVILclv1ghdmowvKX+iT9gS42bhYLw9hcQg= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/namedotcom/go/v4 v4.0.2 h1:4gNkPaPRG/2tqFNUUof7jAVsA6vDutFutEOd7ivnDwA= github.com/namedotcom/go/v4 v4.0.2/go.mod h1:J6sVueHMb0qbarPgdhrzEVhEaYp+R1SCaTGl2s6/J1Q= github.com/nats-io/jwt v1.2.2/go.mod h1:/xX356yQA6LuXI9xWW7mZNpxgF2mBmGecH+Fj34sP5Q= github.com/nats-io/jwt/v2 v2.0.3/go.mod h1:VRP+deawSXyhNjXmxPCHskrR6Mq50BqpEI5SEcNiGlY= github.com/nats-io/nats-server/v2 v2.5.0/go.mod h1:Kj86UtrXAL6LwYRA6H4RqzkHhK0Vcv2ZnKD5WbQ1t3g= github.com/nats-io/nats.go v1.12.1/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= github.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s= github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nrdcg/bunny-go v0.1.0 h1:GAHTRpHaG/TxfLZlqoJ8OJFzw8rI74+jOTkzxWh0uHA= github.com/nrdcg/bunny-go v0.1.0/go.mod h1:u+C9dgsspgtWVaAz6QkyV17s9fxD8viwwKoxb9XMz1A= github.com/nrdcg/desec v0.11.1 h1:ilpKmCr4gGsLcyq3RHfHNmlRzm9fzT2XbWxoVaUCS0s= github.com/nrdcg/desec v0.11.1/go.mod h1:2LuxHlOcwML/7cntu0eimONmA1U+ZxFDAonoSXr4igQ= github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0= github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg= github.com/nrdcg/namesilo v0.5.0 h1:6QNxT/XxE+f5B+7QlfWorthNzOzcGlBLRQxqi6YeBrE= github.com/nrdcg/namesilo v0.5.0/go.mod h1:4UkwlwQfDt74kSGmhLaDylnBrD94IfflnpoEaj6T2qw= github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw= github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.2 h1:HFB2fbVIlhIfCfOW81bZFbiC/RvnpXSdhbF2/DJr134= github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE= github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE= github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0 h1:2nosf3P75OZv2/ZO/9Px5ZgZ5gbKrzA3joN1QMfOGMQ= github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0/go.mod h1:lAVhWwbNaveeJmxrxuSTxMgKpF6DjnuVpn6T8WiBwYQ= github.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM= github.com/peterhellberg/link v1.2.0 h1:UA5pg3Gp/E0F2WdX7GERiNrPQrM1K6CVJUUWfHa4t6c= github.com/peterhellberg/link v1.2.0/go.mod h1:gYfAh+oJgQu2SrZHg5hROVRQe1ICoK0/HHJTcE0edxc= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc= github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE= github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pocketbase/dbx v1.12.0 h1:/oLErM+A0b4xI0PWTGPqSDVjzix48PqI/bng2l0PzoA= github.com/pocketbase/dbx v1.12.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs= github.com/pocketbase/pocketbase v0.36.5 h1:QQhfBtOvKN4VXTwh8is5TLxMOKhPXaUcM/RKAlZ31n0= github.com/pocketbase/pocketbase v0.36.5/go.mod h1:m3tkFYh/+m6yiWHv5ED8gJczVefkbTzrlZOtsNa+bA4= 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-20250701154629-777cf82de5df h1:zEgSHrxo8f6hGG1xCaqunfBq8hlfDmFd1JM0QXiQi7o= github.com/povsister/scp v0.0.0-20250701154629-777cf82de5df/go.mod h1:CiJNEeV6v0tUCNul/+gTjl+FgjfImoiuptJB9AEzqjE= github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/qiniu/dyn v1.3.0 h1:s+xPTeV0H8yikgM4ZMBc7Rrefam8UNI3asBlkaOQg5o= github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk= github.com/qiniu/go-sdk/v7 v7.25.6 h1:89KQX16Bv2x7MxhwpzWGGvQBOPIlGpAcnPQyfS3tRok= github.com/qiniu/go-sdk/v7 v7.25.6/go.mod h1:dmKtJ2ahhPWFVi9o1D5GemmWoh/ctuB9peqTowyTO8o= github.com/qiniu/x v1.10.5 h1:7V/CYWEmo9axJULvrJN6sMYh2FdY+esN5h8jwDkA4b0= github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0= github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/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= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.3.36 h1:zYKBmpT6l7k37LBncd4a1Qj1RvxYFAPf+I6NP5DRBOk= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.3.36/go.mod h1:zTuaHstR5s1J+qxKh4gbQldbKkaZXefxjWUV+bn01T4= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.3.45 h1:pVHnFf2G6Bw2POiX+JrO1yVCFJAPJZ3hL2xqTtnOdRQ= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.3.45/go.mod h1:mW2Ak0kGPxFjzsXArhQaYTZbIIVb1iMw/EaZ3SfPZMg= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.24/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.29/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.34/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.36/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.38/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.42/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.45/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.46/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48 h1:bCs+z6dxRaHWm/C1D/XkSOcCZ0+W2+/6HmIXjpAj+fY= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.48/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap v1.3.34 h1:/QJeztyMC2tYPJceIoObx7LZqqgFcdDM0SQ/Wd0RtEI= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap v1.3.34/go.mod h1:LiTqyLKs+CUdXeiTezJrsMcgi1RhVQ2gFuCcDxQBK9U= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/live v1.3.45 h1:yKIsmuQPgopARN20hGyOwPS059X8wVJEQjnxmpvZc70= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/live v1.3.45/go.mod h1:TCp0N1HLhVkaQfnQ+0HZRChEIsu4hKTzYs/ISVb3cbU= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/scf v1.3.29 h1:MxY5dIlW9e48lqyMc9xtPCmO0RlJJ+RgZMqs6yYte9w= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/scf v1.3.29/go.mod h1:ERH6Ek8rbThvxvqoC91U6ae+qJyUGrXPjv8sw881hho= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.3.42 h1:tzs/LQUXA/RcKP/37WQzL0EXFfWayfx3IESNEgOQmZY= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.3.42/go.mod h1:+OiMLoEYiI3UnjZbf0XBdhLn8chpAupH7/zevjXBFug= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.3.45 h1:7Hw9bVpwApnPuC6GwPb2HO1Mk+lxVqZMjI8n4IK+xRg= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.3.45/go.mod h1:m9gQ6S01nvnzPkeIWmLPWbNI2AiXIfBKQIBY+wwUUeg= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vod v1.3.46 h1:tSCbSFCPgWUXsmmZb9j9ZTGkaH8IpRQCRA5bEMh2CqI= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vod v1.3.46/go.mod h1:HAFMznaORzlFmPFo4kK2+pQ+gdZB3r6ZheBC76jYegE= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/waf v1.3.46 h1:eG0Tfqw3XCj0Tfw7/3HsBLApBjHkBKWzrB3XMfquxwM= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/waf v1.3.46/go.mod h1:V2tffbp8V/mW3fsBgoBmKg55oOim6zymhvfizJsWZlE= 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 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY= github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ucloud/ucloud-sdk-go v0.22.59 h1:9wPpKn5kAnG87QS8oiLjtbyS+oSRPKCzA3JmjUa687c= github.com/ucloud/ucloud-sdk-go v0.22.59/go.mod h1:dyLmFHmUfgb4RZKYQP9IArlvQ2pxzFthfhwxRzOEPIw= github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/volcengine/ve-tos-golang-sdk/v2 v2.9.1 h1:32HAAl4KowauWe2Qf8JTQI+WcnMNPQ5tCMkUv5e+FY0= github.com/volcengine/ve-tos-golang-sdk/v2 v2.9.1/go.mod h1:IrjK84IJJTuOZOTMv/P18Ydjy/x+ow7fF7q11jAxXLM= github.com/volcengine/volc-sdk-golang v1.0.23/go.mod h1:AfG/PZRUkHJ9inETvbjNifTDgut25Wbkm2QoYBTbvyU= github.com/volcengine/volc-sdk-golang v1.0.237 h1:hpLKiS2BwDcSBtZWSz034foCbd0h3FrHTKlUMqHIdc4= github.com/volcengine/volc-sdk-golang v1.0.237/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM= github.com/volcengine/volcengine-go-sdk v1.2.15 h1:duhofGY6gVqcMUfvfa2JTo4uvfixH9rASDlJs4TwQJk= github.com/volcengine/volcengine-go-sdk v1.2.15/go.mod h1:oxoVo+A17kvkwPkIeIHPVLjSw7EQAm+l/Vau1YGHN+A= github.com/vultr/govultr/v3 v3.27.0 h1:J8etMyu/Jh5+idMsu2YZpOWmDXXHeW4VZnkYXmJYHx8= github.com/vultr/govultr/v3 v3.27.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY= github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8= github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM= go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0= go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= 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.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc= golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4= 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= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 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= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 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= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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.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= 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.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= 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= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 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= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= 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= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 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/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= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/ns1/ns1-go.v2 v2.17.2 h1:x8YKHqCJWkC/hddfUhw7FRqTG0x3fr/0ZnWYN+i4THs= gopkg.in/ns1/ns1-go.v2 v2.17.2/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw= k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60= k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8= k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o= k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= 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.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= software.sslmate.com/src/go-pkcs12 v0.7.0 h1:Db8W44cB54TWD7stUFFSWxdfpdn6fZVcDl0w3R4RVM0= software.sslmate.com/src/go-pkcs12 v0.7.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= ================================================ FILE: internal/app/app.go ================================================ package app const ( AppName = "Certimate" AppVersion = "0.4.19" AppUserAgent = AppName + "/" + AppVersion ) ================================================ FILE: internal/app/scheduler.go ================================================ package app import ( "sync" "time" _ "time/tzdata" "github.com/pocketbase/pocketbase/tools/cron" ) var scheduler *cron.Cron var schedulerOnce sync.Once func GetScheduler() *cron.Cron { scheduler = GetApp().Cron() schedulerOnce.Do(func() { location, err := time.LoadLocation("Local") if err == nil { scheduler.Stop() scheduler.SetTimezone(location) scheduler.Start() } }) return scheduler } ================================================ FILE: internal/app/singleton.go ================================================ package app import ( "log/slog" "sync" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase" "github.com/pocketbase/pocketbase/core" ) var ( instance core.App intanceOnce sync.Once ) func GetApp() core.App { intanceOnce.Do(func() { pb := pocketbase.NewWithConfig(pocketbase.Config{ HideStartBanner: true, }) pb.RootCmd.Flags().MarkHidden("encryptionEnv") pb.RootCmd.Flags().MarkHidden("queryTimeout") pb.OnBootstrap().BindFunc(func(e *core.BootstrapEvent) error { err := e.Next() if err != nil { return err } settings := pb.Settings() if !settings.Batch.Enabled { settings.Batch.Enabled = true settings.Batch.MaxRequests = 1000 settings.Batch.Timeout = 30 if err := pb.Save(settings); err != nil { return err } } return nil }) instance = pb }) return instance } func GetDB() dbx.Builder { return GetApp().DB() } func GetLogger() *slog.Logger { app := GetApp() if !app.IsBootstrapped() { panic("MUST NOT USE THIS BEFORE APP BOOTSTRAPPED!") } return app.Logger() } ================================================ FILE: internal/certacme/account.go ================================================ package certacme import ( "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "errors" "fmt" "strings" "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/registration" "golang.org/x/sync/singleflight" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/internal/repository" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) var registrationSg singleflight.Group type ACMEAccount = domain.ACMEAccount func NewACMEAccount(config *ACMEConfig, email string, register bool) (*ACMEAccount, error) { if config == nil { return nil, errors.New("the acme config is nil") } if email == "" { return nil, errors.New("the email is empty") } ctx := context.Background() accountRepo := repository.NewACMEAccountRepository() account, err := accountRepo.GetByCAAndEmail(ctx, string(config.CAProvider), config.CADirUrl, email) if err != nil { if !domain.IsRecordNotFoundError(err) { return nil, fmt.Errorf("failed to get acme account record: %w", err) } } // register new acme account if not exists if account == nil { if !register { return nil, errors.New("the acme account does not exist") } key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return nil, err } keyPEM, err := xcert.ConvertECPrivateKeyToPEM(key) if err != nil { return nil, err } account = &ACMEAccount{ CA: string(config.CAProvider), Email: email, PrivateKey: keyPEM, ACMEDirUrl: config.CADirUrl, } legoCfg := lego.NewConfig(account) legoCfg.CADirURL = config.CADirUrl legoClient, err := lego.NewClient(legoCfg) if err != nil { return nil, err } var regres *registration.Resource var regerr error if legoClient.GetExternalAccountRequired() { if config.EABKid == "" { return nil, errors.New("missing or invalid eab kid") } if config.EABHmacKey == "" { return nil, errors.New("missing or invalid eab hmac key") } // patch, see https://github.com/go-acme/lego/issues/2634 keyId := strings.TrimSpace(config.EABKid) keyEncoded := strings.TrimSpace(config.EABHmacKey) keyEncoded = strings.ReplaceAll(strings.ReplaceAll(keyEncoded, "+", "-"), "/", "_") keyEncoded = strings.TrimRight(keyEncoded, "=") regres, regerr = legoClient.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ TermsOfServiceAgreed: true, Kid: keyId, HmacEncoded: keyEncoded, }) } else { regres, regerr = legoClient.Registration.Register(registration.RegisterOptions{ TermsOfServiceAgreed: true, }) } if regerr != nil { return nil, fmt.Errorf("failed to register acme account: %w", regerr) } account.ACMEAccount = ®res.Body account.ACMEAcctUrl = regres.URI if _, err := accountRepo.Save(ctx, account); err != nil { return nil, fmt.Errorf("failed to save acme account record: %w", err) } } return account, nil } func NewACMEAccountWithSingleFlight(config *ACMEConfig, email string) (*ACMEAccount, error) { if config == nil { return nil, errors.New("the acme config is nil") } if email == "" { return nil, errors.New("the email is empty") } resp, err, _ := registrationSg.Do(fmt.Sprintf("%s|%s|%s", string(config.CAProvider), config.CADirUrl, email), func() (any, error) { return NewACMEAccount(config, email, true) }) if err != nil { return nil, err } return resp.(*ACMEAccount), nil } ================================================ FILE: internal/certacme/certifiers/registry.go ================================================ package certifiers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ProviderFactoryFunc func(options *ProviderFactoryOptions) (certifier.ACMEChallenger, error) type ProviderFactoryOptions struct { ProviderAccessConfig map[string]any ProviderExtendedConfig map[string]any DnsPropagationTimeout int DnsTTL int } type Registry[T comparable] interface { Register(T, ProviderFactoryFunc) error RegisterAlias(T, T) error MustRegister(T, ProviderFactoryFunc) MustRegisterAlias(T, T) Get(T) (ProviderFactoryFunc, error) } type registry[T comparable] struct { factories map[T]ProviderFactoryFunc } func (r *registry[T]) Register(name T, factory ProviderFactoryFunc) error { if _, exists := r.factories[name]; exists { return fmt.Errorf("provider '%v' already registered", name) } r.factories[name] = factory return nil } func (r *registry[T]) RegisterAlias(name T, alias T) error { factory, err := r.Get(alias) if err != nil { return err } err = r.Register(name, factory) if err != nil { return err } return nil } func (r *registry[T]) MustRegister(name T, factory ProviderFactoryFunc) { if err := r.Register(name, factory); err != nil { panic(err) } } func (r *registry[T]) MustRegisterAlias(name T, alias T) { if err := r.RegisterAlias(name, alias); err != nil { panic(err) } } func (r *registry[T]) Get(name T) (ProviderFactoryFunc, error) { if factory, exists := r.factories[name]; exists { return factory, nil } return nil, fmt.Errorf("provider '%v' not registered", name) } func newRegistry[T comparable]() Registry[T] { return ®istry[T]{factories: make(map[T]ProviderFactoryFunc)} } var ( ACMEDns01Registries = newRegistry[domain.ACMEDns01ProviderType]() ACMEHttp01Registries = newRegistry[domain.ACMEHttp01ProviderType]() ) ================================================ FILE: internal/certacme/certifiers/sp_35cn.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" west35cn "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/35cn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderType35cn, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigFor35cn{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := west35cn.NewChallenger(&west35cn.ChallengerConfig{ Username: credentials.Username, ApiPassword: credentials.ApiPassword, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_51dnscom.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" dnscom "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/51dnscom" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderType51DNScom, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigFor51DNScom{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := dnscom.NewChallenger(&dnscom.ChallengerConfig{ ApiKey: credentials.ApiKey, ApiSecret: credentials.ApiSecret, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_acmedns.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/acmedns" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeACMEDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForACMEDNS{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := acmedns.NewChallenger(&acmedns.ChallengerConfig{ ServerUrl: credentials.ServerUrl, Credentials: credentials.Credentials, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_acmehttpreq.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/acmehttpreq" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeACMEHttpReq, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForACMEHttpReq{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := acmehttpreq.NewChallenger(&acmehttpreq.ChallengerConfig{ Endpoint: credentials.Endpoint, Mode: credentials.Mode, Username: credentials.Username, Password: credentials.Password, DnsPropagationTimeout: options.DnsPropagationTimeout, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_akamai_edgedns.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" akamaiedgedns "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/akamai-edgedns" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeAkamaiEdgeDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForAkamai{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := akamaiedgedns.NewChallenger(&akamaiedgedns.ChallengerConfig{ Host: credentials.Host, ClientToken: credentials.ClientToken, ClientSecret: credentials.ClientSecret, AccessToken: credentials.AccessToken, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) ACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeAkamai, domain.ACMEDns01ProviderTypeAkamaiEdgeDNS) } ================================================ FILE: internal/certacme/certifiers/sp_aliyun_dns.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/aliyun" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeAliyunDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForAliyun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := aliyun.NewChallenger(&aliyun.ChallengerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) ACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeAliyun, domain.ACMEDns01ProviderTypeAliyunDNS) } ================================================ FILE: internal/certacme/certifiers/sp_aliyun_esa.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" aliyunesa "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/aliyun-esa" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeAliyunESA, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForAliyun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := aliyunesa.NewChallenger(&aliyunesa.ChallengerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_arvancloud.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/arvancloud" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeArvanCloud, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForArvanCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := arvancloud.NewChallenger(&arvancloud.ChallengerConfig{ ApiKey: credentials.ApiKey, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_aws_route53.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" awsroute53 "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/aws-route53" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeAWSRoute53, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForAWS{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := awsroute53.NewChallenger(&awsroute53.ChallengerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), HostedZoneId: xmaps.GetString(options.ProviderExtendedConfig, "hostedZoneId"), DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) ACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeAWS, domain.ACMEDns01ProviderTypeAWSRoute53) } ================================================ FILE: internal/certacme/certifiers/sp_azure_dns.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" azuredns "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/azure-dns" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeAzureDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForAzure{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := azuredns.NewChallenger(&azuredns.ChallengerConfig{ TenantId: credentials.TenantId, ClientId: credentials.ClientId, ClientSecret: credentials.ClientSecret, SubscriptionId: credentials.SubscriptionId, ResourceGroupName: credentials.ResourceGroupName, CloudName: credentials.CloudName, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) ACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeAzure, domain.ACMEDns01ProviderTypeAzureDNS) } ================================================ FILE: internal/certacme/certifiers/sp_baiducloud_dns.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/baiducloud" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeBaiduCloudDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForBaiduCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := baiducloud.NewChallenger(&baiducloud.ChallengerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) ACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeBaiduCloud, domain.ACMEDns01ProviderTypeBaiduCloudDNS) } ================================================ FILE: internal/certacme/certifiers/sp_bookmyname.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/bookmyname" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeBookMyName, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForBookMyName{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := bookmyname.NewChallenger(&bookmyname.ChallengerConfig{ Username: credentials.Username, Password: credentials.Password, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_bunny.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/bunny" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeBunny, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForBunny{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := bunny.NewChallenger(&bunny.ChallengerConfig{ ApiKey: credentials.ApiKey, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_cloudflare.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/cloudflare" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeCloudflare, func(options *ProviderFactoryOptions) (challenge.Provider, 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 := cloudflare.NewChallenger(&cloudflare.ChallengerConfig{ DnsApiToken: credentials.DnsApiToken, ZoneApiToken: credentials.ZoneApiToken, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_cloudns.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/cloudns" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeClouDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForClouDNS{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := cloudns.NewChallenger(&cloudns.ChallengerConfig{ AuthId: credentials.AuthId, AuthPassword: credentials.AuthPassword, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_cmcccloud_dns.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/cmcccloud" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeCMCCCloudDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForCMCCCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := cmcccloud.NewChallenger(&cmcccloud.ChallengerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) ACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeCMCCCloud, domain.ACMEDns01ProviderTypeCMCCCloudDNS) } ================================================ FILE: internal/certacme/certifiers/sp_constellix.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" constellix "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/constellix" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeConstellix, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForConstellix{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := constellix.NewChallenger(&constellix.ChallengerConfig{ ApiKey: credentials.ApiKey, SecretKey: credentials.SecretKey, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_cpanel.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/cpanel" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeCPanel, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForCPanel{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := cpanel.NewChallenger(&cpanel.ChallengerConfig{ ServerUrl: credentials.ServerUrl, Username: credentials.Username, ApiToken: credentials.ApiToken, AllowInsecureConnections: credentials.AllowInsecureConnections, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_ctcccloud_smartdns.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/ctcccloud" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeCTCCCloudSmartDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForCTCCCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := ctcccloud.NewChallenger(&ctcccloud.ChallengerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) ACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeCTCCCloud, domain.ACMEDns01ProviderTypeCTCCCloudSmartDNS) } ================================================ FILE: internal/certacme/certifiers/sp_desec.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/desec" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeDeSEC, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForDeSEC{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := desec.NewChallenger(&desec.ChallengerConfig{ Token: credentials.Token, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_digitalocean.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" digitalocean "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/digitalocean" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeDigitalOcean, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForDigitalOcean{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := digitalocean.NewChallenger(&digitalocean.ChallengerConfig{ AccessToken: credentials.AccessToken, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_dnsexit.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/dnsexit" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeDNSExit, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForDNSExit{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := dnsexit.NewChallenger(&dnsexit.ChallengerConfig{ ApiKey: credentials.ApiKey, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_dnsla.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/dnsla" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeDNSLA, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForDNSLA{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := dnsla.NewChallenger(&dnsla.ChallengerConfig{ ApiId: credentials.ApiId, ApiSecret: credentials.ApiSecret, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_dnsmadeeasy.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/dnsmadeeasy" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeDNSMadeEasy, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForDNSMadeEasy{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := dnsmadeeasy.NewChallenger(&dnsmadeeasy.ChallengerConfig{ ApiKey: credentials.ApiKey, ApiSecret: credentials.ApiSecret, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_duckdns.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" duckdns "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/duckdns" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeDuckDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForDuckDNS{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := duckdns.NewChallenger(&duckdns.ChallengerConfig{ Token: credentials.Token, DnsPropagationTimeout: options.DnsPropagationTimeout, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_dynu.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/dynu" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeDynu, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForDynu{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := dynu.NewChallenger(&dynu.ChallengerConfig{ ApiKey: credentials.ApiKey, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_dynv6.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/dynv6" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeDynv6, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForDynv6{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := dynv6.NewChallenger(&dynv6.ChallengerConfig{ HttpToken: credentials.HttpToken, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_gandinet.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/gandinet" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeGandinet, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForGandinet{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := gandinet.NewChallenger(&gandinet.ChallengerConfig{ PersonalAccessToken: credentials.PersonalAccessToken, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_gcore.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/gcore" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeGcore, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForGcore{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := gcore.NewChallenger(&gcore.ChallengerConfig{ ApiToken: credentials.ApiToken, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_gname.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/gname" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeGname, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForGname{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := gname.NewChallenger(&gname.ChallengerConfig{ AppId: credentials.AppId, AppKey: credentials.AppKey, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_godaddy.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/godaddy" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeGoDaddy, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForGoDaddy{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := godaddy.NewChallenger(&godaddy.ChallengerConfig{ ApiKey: credentials.ApiKey, ApiSecret: credentials.ApiSecret, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_hetzner.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/hetzner" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeHetzner, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForHetzner{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := hetzner.NewChallenger(&hetzner.ChallengerConfig{ ApiToken: credentials.ApiToken, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_hostingde.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/hostingde" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeHostingde, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForHostingde{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := hostingde.NewChallenger(&hostingde.ChallengerConfig{ ApiKey: credentials.ApiKey, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_hostinger.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/hostinger" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeHostinger, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForHostinger{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := hostinger.NewChallenger(&hostinger.ChallengerConfig{ ApiToken: credentials.ApiToken, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_huaweicloud_dns.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/huaweicloud" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeHuaweiCloudDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForHuaweiCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := huaweicloud.NewChallenger(&huaweicloud.ChallengerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) ACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeHuaweiCloud, domain.ACMEDns01ProviderTypeHuaweiCloudDNS) } ================================================ FILE: internal/certacme/certifiers/sp_infomaniak.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/infomaniak" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeInfomaniak, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForInfomaniak{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := infomaniak.NewChallenger(&infomaniak.ChallengerConfig{ AccessToken: credentials.AccessToken, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_ionos.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/ionos" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeIONOS, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForIONOS{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := ionos.NewChallenger(&ionos.ChallengerConfig{ ApiKeyPublicPrefix: credentials.ApiKeyPublicPrefix, ApiKeySecret: credentials.ApiKeySecret, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_jdcloud_dns.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/jdcloud" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeJDCloudDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForJDCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := jdcloud.NewChallenger(&jdcloud.ChallengerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, RegionId: xmaps.GetString(options.ProviderExtendedConfig, "regionId"), DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) ACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeJDCloud, domain.ACMEDns01ProviderTypeJDCloudDNS) } ================================================ FILE: internal/certacme/certifiers/sp_linode.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/linode" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeLinode, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForLinode{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := linode.NewChallenger(&linode.ChallengerConfig{ AccessToken: credentials.AccessToken, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_local.go ================================================ package certifiers import ( "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/http01/local" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEHttp01Registries.MustRegister(domain.ACMEHttp01ProviderTypeLocal, func(options *ProviderFactoryOptions) (challenge.Provider, error) { provider, err := local.NewChallenger(&local.ChallengerConfig{ WebRootPath: xmaps.GetString(options.ProviderExtendedConfig, "webRootPath"), }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_namecheap.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" namecheap "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/namecheap" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeNamecheap, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForNamecheap{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := namecheap.NewChallenger(&namecheap.ChallengerConfig{ Username: credentials.Username, ApiKey: credentials.ApiKey, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_namedotcom.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/namedotcom" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeNameDotCom, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForNameDotCom{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := namedotcom.NewChallenger(&namedotcom.ChallengerConfig{ Username: credentials.Username, ApiToken: credentials.ApiToken, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_namesilo.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/namesilo" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeNameSilo, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForNameSilo{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := namesilo.NewChallenger(&namesilo.ChallengerConfig{ ApiKey: credentials.ApiKey, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_netcup.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/netcup" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeNetcup, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForNetcup{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := netcup.NewChallenger(&netcup.ChallengerConfig{ CustomerNumber: credentials.CustomerNumber, ApiKey: credentials.ApiKey, ApiPassword: credentials.ApiPassword, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_netlify.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" netlify "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/netlify" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeNetlify, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForNetlify{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := netlify.NewChallenger(&netlify.ChallengerConfig{ ApiToken: credentials.ApiToken, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_ns1.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/ns1" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeNS1, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForNS1{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := ns1.NewChallenger(&ns1.ChallengerConfig{ ApiKey: credentials.ApiKey, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_ovhcloud.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/ovhcloud" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeOVHcloud, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForOVHcloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := ovhcloud.NewChallenger(&ovhcloud.ChallengerConfig{ Endpoint: credentials.Endpoint, AuthMethod: credentials.AuthMethod, ApplicationKey: credentials.ApplicationKey, ApplicationSecret: credentials.ApplicationSecret, ConsumerKey: credentials.ConsumerKey, ClientId: credentials.ClientId, ClientSecret: credentials.ClientSecret, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_porkbun.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/porkbun" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypePorkbun, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForPorkbun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := porkbun.NewChallenger(&porkbun.ChallengerConfig{ ApiKey: credentials.ApiKey, SecretApiKey: credentials.SecretApiKey, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_powerdns.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/powerdns" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypePowerDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForPowerDNS{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := powerdns.NewChallenger(&powerdns.ChallengerConfig{ ServerUrl: credentials.ServerUrl, ApiKey: credentials.ApiKey, AllowInsecureConnections: credentials.AllowInsecureConnections, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_qingcloud_dns.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/qingcloud" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeQingCloudDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForQingCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := qingcloud.NewChallenger(&qingcloud.ChallengerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) ACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeQingCloud, domain.ACMEDns01ProviderTypeQingCloudDNS) } ================================================ FILE: internal/certacme/certifiers/sp_rainyun.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/rainyun" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeRainYun, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForRainYun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := rainyun.NewChallenger(&rainyun.ChallengerConfig{ ApiKey: credentials.ApiKey, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_rfc2136.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/rfc2136" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeRFC2136, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForRFC2136{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := rfc2136.NewChallenger(&rfc2136.ChallengerConfig{ Host: credentials.Host, Port: credentials.Port, TsigAlgorithm: credentials.TsigAlgorithm, TsigKey: credentials.TsigKey, TsigSecret: credentials.TsigSecret, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_s3.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/http01/s3" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEHttp01Registries.MustRegister(domain.ACMEHttp01ProviderTypeS3, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForS3{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := s3.NewChallenger(&s3.ChallengerConfig{ Endpoint: credentials.Endpoint, AccessKey: credentials.AccessKey, SecretKey: credentials.SecretKey, SignatureVersion: credentials.SignatureVersion, UsePathStyle: credentials.UsePathStyle, AllowInsecureConnections: credentials.AllowInsecureConnections, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), Bucket: xmaps.GetString(options.ProviderExtendedConfig, "bucket"), }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_spaceship.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/spaceship" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeSpaceship, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForSpaceship{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := spaceship.NewChallenger(&spaceship.ChallengerConfig{ ApiKey: credentials.ApiKey, ApiSecret: credentials.ApiSecret, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_ssh.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/http01/ssh" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEHttp01Registries.MustRegister(domain.ACMEHttp01ProviderTypeSSH, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForSSH{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } jumpServers := make([]ssh.ServerConfig, len(credentials.JumpServers)) for i, jumpServer := range credentials.JumpServers { jumpServers[i] = ssh.ServerConfig{ SshHost: jumpServer.Host, SshPort: jumpServer.Port, SshAuthMethod: jumpServer.AuthMethod, SshUsername: jumpServer.Username, SshPassword: jumpServer.Password, SshKey: jumpServer.Key, SshKeyPassphrase: jumpServer.KeyPassphrase, } } provider, err := ssh.NewChallenger(&ssh.ChallengerConfig{ ServerConfig: ssh.ServerConfig{ SshHost: credentials.Host, SshPort: credentials.Port, SshAuthMethod: credentials.AuthMethod, SshUsername: credentials.Username, SshPassword: credentials.Password, SshKey: credentials.Key, SshKeyPassphrase: credentials.KeyPassphrase, }, JumpServers: jumpServers, UseSCP: xmaps.GetBool(options.ProviderExtendedConfig, "useSCP"), WebRootPath: xmaps.GetString(options.ProviderExtendedConfig, "webRootPath"), }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_technitiumdns.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/technitiumdns" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeTechnitiumDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForTechnitiumDNS{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := technitiumdns.NewChallenger(&technitiumdns.ChallengerConfig{ ServerUrl: credentials.ServerUrl, ApiToken: credentials.ApiToken, AllowInsecureConnections: credentials.AllowInsecureConnections, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_tencentcloud_dns.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/tencentcloud" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeTencentCloudDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForTencentCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := tencentcloud.NewChallenger(&tencentcloud.ChallengerConfig{ SecretId: credentials.SecretId, SecretKey: credentials.SecretKey, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) ACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeTencentCloud, domain.ACMEDns01ProviderTypeTencentCloudDNS) } ================================================ FILE: internal/certacme/certifiers/sp_tencentcloud_eo.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" teo "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/tencentcloud-eo" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeTencentCloudEO, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForTencentCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := teo.NewChallenger(&teo.ChallengerConfig{ SecretId: credentials.SecretId, SecretKey: credentials.SecretKey, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_todaynic.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/todaynic" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeTodayNIC, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForTodayNIC{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := todaynic.NewChallenger(&todaynic.ChallengerConfig{ UserId: credentials.UserId, ApiKey: credentials.ApiKey, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_ucloud_udnr.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/ucloud" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeUCloudUDNR, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForUCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := ucloud.NewChallenger(&ucloud.ChallengerConfig{ PrivateKey: credentials.PrivateKey, PublicKey: credentials.PublicKey, ProjectId: credentials.ProjectId, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) ACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeUCloud, domain.ACMEDns01ProviderTypeUCloudUDNR) } ================================================ FILE: internal/certacme/certifiers/sp_vercel.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/vercel" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeVercel, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForVercel{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := vercel.NewChallenger(&vercel.ChallengerConfig{ ApiAccessToken: credentials.ApiAccessToken, TeamId: credentials.TeamId, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_volcengine_dns.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/volcengine" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeVolcEngineDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForVolcEngine{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := volcengine.NewChallenger(&volcengine.ChallengerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) ACMEDns01Registries.MustRegisterAlias(domain.ACMEDns01ProviderTypeVolcEngine, domain.ACMEDns01ProviderTypeVolcEngineDNS) } ================================================ FILE: internal/certacme/certifiers/sp_vultr.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/vultr" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeVultr, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForVultr{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := vultr.NewChallenger(&vultr.ChallengerConfig{ ApiKey: credentials.ApiKey, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_westcn.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/westcn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeWestcn, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForWestcn{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := westcn.NewChallenger(&westcn.ChallengerConfig{ Username: credentials.Username, ApiPassword: credentials.ApiPassword, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/certifiers/sp_xinnet.go ================================================ package certifiers import ( "fmt" "github.com/go-acme/lego/v4/challenge" "github.com/certimate-go/certimate/internal/domain" xinnet "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/xinnet" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeXinnet, func(options *ProviderFactoryOptions) (challenge.Provider, error) { credentials := domain.AccessConfigForXinnet{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := xinnet.NewChallenger(&xinnet.ChallengerConfig{ AgentId: credentials.AgentId, ApiPassword: credentials.ApiPassword, DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) return provider, err }) } ================================================ FILE: internal/certacme/client.go ================================================ package certacme import ( "context" "errors" "time" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/internal/repository" "github.com/go-acme/lego/v4/lego" ) type ACMEClient struct { client *lego.Client account *ACMEAccount } func NewACMEClient(config *ACMEConfig, email string, configures ...func(*lego.Config) error) (*ACMEClient, error) { account, err := NewACMEAccountWithSingleFlight(config, email) if err != nil { return nil, err } mergedConfigures := []func(*lego.Config) error{ func(legoCfg *lego.Config) error { legoCfg.CADirURL = config.CADirUrl legoCfg.Certificate.KeyType = config.CertifierKeyType return nil }, } mergedConfigures = append(mergedConfigures, configures...) return newACMEClientWithAccount(account, mergedConfigures...) } func NewACMEClientWithAccount(account *ACMEAccount, configures ...func(*lego.Config) error) (*ACMEClient, error) { return newACMEClientWithAccount(account, configures...) } func newACMEClientWithAccount(account *ACMEAccount, configures ...func(*lego.Config) error) (*ACMEClient, error) { if account == nil { return nil, errors.New("the acme account is nil") } legoCfg := lego.NewConfig(account) legoCfg.CADirURL = account.ACMEDirUrl settingsRepo := repository.NewSettingsRepository() settings, _ := settingsRepo.GetByName(context.Background(), domain.SettingsNameSSLProvider) if settings != nil { sslProviderSettings := settings.Content.AsSSLProvider() if sslProviderSettings.Timeout > 0 { legoCfg.Certificate.Timeout = time.Duration(sslProviderSettings.Timeout) * time.Second } } errs := make([]error, 0) for _, configure := range configures { if err := configure(legoCfg); err != nil { errs = append(errs, err) } } if len(errs) > 0 { return nil, errors.Join(errs...) } legoClient, err := lego.NewClient(legoCfg) if err != nil { return nil, err } return &ACMEClient{ client: legoClient, account: account, }, nil } ================================================ FILE: internal/certacme/client_obtain.go ================================================ package certacme import ( "context" "crypto" "errors" "fmt" "os" "strconv" "strings" "time" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/challenge/http01" "github.com/go-acme/lego/v4/log" "github.com/samber/lo" "github.com/certimate-go/certimate/internal/certacme/certifiers" "github.com/certimate-go/certimate/internal/domain" ) type ObtainCertificateRequest struct { DomainOrIPs []string PrivateKeyType certcrypto.KeyType PrivateKeyPEM string ValidityNotBefore time.Time ValidityNotAfter time.Time NoCommonName bool // 提供商相关 ChallengeType string Provider string ProviderAccessConfig map[string]any ProviderExtendedConfig map[string]any // 解析相关 DisableFollowCNAME bool Nameservers []string // DNS-01 质询相关 DnsPropagationWait int DnsPropagationTimeout int DnsTTL int // HTTP-01 质询相关 HttpDelayWait int // ACME 相关 PreferredChain string ACMEProfile string // ARI 相关 ARIReplacesAcctUrl string ARIReplacesCertId string } type ObtainCertificateResponse struct { CSR string FullChainCertificate string IssuerCertificate string PrivateKey string ACMEAcctUrl string ACMECertUrl string ARIReplaced bool } func (c *ACMEClient) ObtainCertificate(ctx context.Context, request *ObtainCertificateRequest) (*ObtainCertificateResponse, error) { type result struct { res *ObtainCertificateResponse err error } done := make(chan result, 1) go func() { res, err := c.sendObtainCertificateRequest(request) done <- result{res, err} }() select { case <-ctx.Done(): return nil, ctx.Err() case r := <-done: return r.res, r.err } } func (c *ACMEClient) sendObtainCertificateRequest(request *ObtainCertificateRequest) (*ObtainCertificateResponse, error) { if request == nil { return nil, errors.New("the request is nil") } os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", strconv.FormatBool(request.DisableFollowCNAME)) switch request.ChallengeType { case "dns-01": { providerFactory, err := certifiers.ACMEDns01Registries.Get(domain.ACMEDns01ProviderType(request.Provider)) if err != nil { return nil, err } provider, err := providerFactory(&certifiers.ProviderFactoryOptions{ ProviderAccessConfig: request.ProviderAccessConfig, ProviderExtendedConfig: request.ProviderExtendedConfig, DnsPropagationTimeout: request.DnsPropagationTimeout, DnsTTL: request.DnsTTL, }) if err != nil { return nil, fmt.Errorf("failed to initialize dns-01 provider '%s': %w", request.Provider, err) } c.client.Challenge.SetDNS01Provider(provider, dns01.CondOption( len(request.Nameservers) > 0, dns01.AddRecursiveNameservers(dns01.ParseNameservers(request.Nameservers)), ), dns01.CondOption( request.DnsPropagationWait > 0, dns01.PropagationWait(time.Duration(request.DnsPropagationWait)*time.Second, true), ), dns01.CondOption( len(request.Nameservers) > 0 || request.DnsPropagationWait > 0, dns01.DisableAuthoritativeNssPropagationRequirement(), ), ) } case "http-01": { providerFactory, err := certifiers.ACMEHttp01Registries.Get(domain.ACMEHttp01ProviderType(request.Provider)) if err != nil { return nil, err } provider, err := providerFactory(&certifiers.ProviderFactoryOptions{ ProviderAccessConfig: request.ProviderAccessConfig, ProviderExtendedConfig: request.ProviderExtendedConfig, }) if err != nil { return nil, fmt.Errorf("failed to initialize http-01 provider '%s': %w", request.Provider, err) } c.client.Challenge.SetHTTP01Provider(provider, http01.SetDelay(time.Duration(request.HttpDelayWait)*time.Second), ) } default: return nil, fmt.Errorf("unsupported challenge type: '%s'", request.ChallengeType) } var privkey crypto.PrivateKey if request.PrivateKeyPEM != "" { pk, err := certcrypto.ParsePEMPrivateKey([]byte(request.PrivateKeyPEM)) if err != nil { return nil, fmt.Errorf("failed to parse private key: %w", err) } privkey = pk } req := certificate.ObtainRequest{ Domains: request.DomainOrIPs, PrivateKey: privkey, Bundle: true, PreferredChain: request.PreferredChain, Profile: request.ACMEProfile, NotBefore: request.ValidityNotBefore, NotAfter: request.ValidityNotAfter, ReplacesCertID: lo.If(request.ARIReplacesAcctUrl == c.account.ACMEAcctUrl, request.ARIReplacesCertId).Else(""), } resp, err := c.client.Certificate.Obtain(req) if err != nil { ariErr := &acme.AlreadyReplacedError{} if !errors.As(err, &ariErr) { return nil, err } log.Warnf("the certificate has already been replaced, try to obtain again without ARI ...") // reset ARI and retry if failure req.ReplacesCertID = "" resp, err = c.client.Certificate.Obtain(req) if err != nil { return nil, err } } return &ObtainCertificateResponse{ CSR: strings.TrimSpace(string(resp.CSR)), FullChainCertificate: strings.TrimSpace(string(resp.Certificate)), IssuerCertificate: strings.TrimSpace(string(resp.IssuerCertificate)), PrivateKey: strings.TrimSpace(string(resp.PrivateKey)), ACMEAcctUrl: c.account.ACMEAcctUrl, ACMECertUrl: resp.CertURL, ARIReplaced: req.ReplacesCertID != "", }, nil } ================================================ FILE: internal/certacme/client_revoke.go ================================================ package certacme import ( "context" "errors" ) type RevokeCertificateRequest struct { Certificate string } type RevokeCertificateResponse struct{} func (c *ACMEClient) RevokeCertificate(ctx context.Context, request *RevokeCertificateRequest) (*RevokeCertificateResponse, error) { type result struct { res *RevokeCertificateResponse err error } done := make(chan result, 1) go func() { res, err := c.sendRevokeCertificateRequest(request) done <- result{res, err} }() select { case <-ctx.Done(): return nil, ctx.Err() case r := <-done: return r.res, r.err } } func (c *ACMEClient) sendRevokeCertificateRequest(request *RevokeCertificateRequest) (*RevokeCertificateResponse, error) { if request == nil { return nil, errors.New("the request is nil") } err := c.client.Certificate.Revoke([]byte(request.Certificate)) if err != nil { return nil, err } return &RevokeCertificateResponse{}, nil } ================================================ FILE: internal/certacme/config.go ================================================ package certacme import ( "context" "errors" "strings" "github.com/go-acme/lego/v4/certcrypto" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/internal/repository" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) var acmeDirUrls = map[string]string{ string(domain.CAProviderTypeLetsEncrypt): "https://acme-v02.api.letsencrypt.org/directory", string(domain.CAProviderTypeLetsEncryptStaging): "https://acme-staging-v02.api.letsencrypt.org/directory", string(domain.CAProviderTypeActalisSSL): "https://acme-api.actalis.com/acme/directory", string(domain.CAProviderTypeDigiCert): "https://acme.digicert.com/v2/acme/directory", string(domain.CAProviderTypeGlobalSignAtlas): "https://emea.acme.atlas.globalsign.com/directory", string(domain.CAProviderTypeGoogleTrustServices): "https://dv.acme-v02.api.pki.goog/directory", string(domain.CAProviderTypeLiteSSL): "https://acme.litessl.com/acme/v2/directory", string(domain.CAProviderTypeSSLCom): "https://acme.ssl.com/sslcom-dv-rsa", string(domain.CAProviderTypeSSLCom) + "RSA": "https://acme.ssl.com/sslcom-dv-rsa", string(domain.CAProviderTypeSSLCom) + "ECC": "https://acme.ssl.com/sslcom-dv-ecc", string(domain.CAProviderTypeSectigo): "https://acme.sectigo.com/v2/DV", string(domain.CAProviderTypeSectigo) + "DV": "https://acme.sectigo.com/v2/DV", string(domain.CAProviderTypeSectigo) + "OV": "https://acme.sectigo.com/v2/OV", string(domain.CAProviderTypeSectigo) + "EV": "https://acme.sectigo.com/v2/EV", string(domain.CAProviderTypeZeroSSL): "https://acme.zerossl.com/v2/DV90", } type ACMEConfigOptions struct { CAProvider string CAAccessConfig map[string]any CAProviderConfig map[string]any CertifierKeyType certcrypto.KeyType } type ACMEConfig struct { CAProvider domain.CAProviderType CADirUrl string EABKid string EABHmacKey string CertifierKeyType certcrypto.KeyType } func NewACMEConfig(options *ACMEConfigOptions) (*ACMEConfig, error) { if options == nil { return nil, errors.New("the options is nil") } caProvider := options.CAProvider caAccessConfig := options.CAAccessConfig if options.CAProvider == "" { settingsRepo := repository.NewSettingsRepository() settings, _ := settingsRepo.GetByName(context.Background(), domain.SettingsNameSSLProvider) if settings != nil { sslProviderSettings := settings.Content.AsSSLProvider() caProvider = string(sslProviderSettings.Provider) caAccessConfig = sslProviderSettings.Configs[sslProviderSettings.Provider] } } if caProvider == "" { // default CA: Let's Encrypt caProvider = string(domain.AccessProviderTypeLetsEncrypt) } if caAccessConfig == nil { caAccessConfig = make(map[string]any) } ca := &ACMEConfig{CAProvider: domain.CAProviderType(caProvider), CertifierKeyType: options.CertifierKeyType} switch ca.CAProvider { case domain.CAProviderTypeSectigo: credentials := &domain.AccessConfigForGlobalSectigo{} if err := xmaps.Populate(caAccessConfig, &credentials); err != nil { return nil, err } else if strings.EqualFold(credentials.ValidationType, "DV") { ca.CADirUrl = acmeDirUrls[string(domain.CAProviderTypeSectigo)+"DV"] } else if strings.EqualFold(credentials.ValidationType, "OV") { ca.CADirUrl = acmeDirUrls[string(domain.CAProviderTypeSectigo)+"OV"] } else if strings.EqualFold(credentials.ValidationType, "EV") { ca.CADirUrl = acmeDirUrls[string(domain.CAProviderTypeSectigo)+"EV"] } else { ca.CADirUrl = acmeDirUrls[string(domain.CAProviderTypeSectigo)] } case domain.CAProviderTypeSSLCom: if strings.HasPrefix(string(options.CertifierKeyType), "RSA") { ca.CADirUrl = acmeDirUrls[string(domain.CAProviderTypeSSLCom)+"RSA"] } else if strings.HasPrefix(string(options.CertifierKeyType), "EC") { ca.CADirUrl = acmeDirUrls[string(domain.CAProviderTypeSSLCom)+"ECC"] } else { ca.CADirUrl = acmeDirUrls[string(domain.CAProviderTypeSSLCom)] } case domain.CAProviderTypeACMECA: credentials := &domain.AccessConfigForACMECA{} if err := xmaps.Populate(caAccessConfig, &credentials); err != nil { return nil, err } else if credentials.Endpoint == "" { return nil, errors.New("the endpoint of custom ACME CA is empty") } ca.CADirUrl = credentials.Endpoint default: endpoint := acmeDirUrls[string(ca.CAProvider)] if endpoint == "" { return nil, errors.New("the endpoint of the ACME CA provider is empty") } ca.CADirUrl = endpoint } eab := domain.AccessConfigForACMEExternalAccountBinding{} if err := xmaps.Populate(caAccessConfig, &eab); err != nil { return nil, err } ca.EABKid = eab.EabKid ca.EABHmacKey = eab.EabHmacKey return ca, nil } ================================================ FILE: internal/certacme/logging.go ================================================ package certacme import ( "fmt" "log" "log/slog" "os" "strings" legolog "github.com/go-acme/lego/v4/log" ) type legoLogger struct { callLogger *slog.Logger legoLogger legolog.StdLogger } func (l *legoLogger) Fatal(args ...any) { l.callLogger.Error("go-acme/lego: " + fmt.Sprint(args...)) l.legoLogger.Fatal(args...) } func (l *legoLogger) Fatalln(args ...any) { l.Fatal(fmt.Sprintln(args...)) } func (l *legoLogger) Fatalf(format string, args ...any) { l.Fatal(fmt.Sprintf(format, args...)) } func (l *legoLogger) Print(args ...any) { message := fmt.Sprint(args...) print := l.callLogger.Debug if strings.HasPrefix(message, "[WARN] ") { message = strings.TrimPrefix(message, "[WARN] ") print = l.callLogger.Warn } else if strings.HasPrefix(message, "[INFO] ") { message = strings.TrimPrefix(message, "[INFO] ") print = l.callLogger.Info } print("go-acme/lego: " + message) l.legoLogger.Print(message) } func (l *legoLogger) Println(args ...any) { l.Print(fmt.Sprintln(args...)) } func (l *legoLogger) Printf(format string, args ...any) { l.Print(fmt.Sprintf(format, args...)) } func NewLegoLogger(logger *slog.Logger) legolog.StdLogger { return &legoLogger{ callLogger: logger, // https://github.com/go-acme/lego/blob/master/log/logger.go legoLogger: log.New(os.Stderr, "", log.LstdFlags), } } ================================================ FILE: internal/certificate/service.go ================================================ package certificate import ( "archive/zip" "bytes" "context" "errors" "fmt" "log/slog" "strings" "github.com/pocketbase/dbx" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/internal/certacme" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/internal/domain/dtos" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertificateService struct { acmeAccountRepo acmeAccountRepository certificateRepo certificateRepository settingsRepo settingsRepository } func NewCertificateService( acmeAccountRepo acmeAccountRepository, certificateRepo certificateRepository, settingsRepo settingsRepository, ) *CertificateService { return &CertificateService{ acmeAccountRepo: acmeAccountRepo, certificateRepo: certificateRepo, settingsRepo: settingsRepo, } } func (s *CertificateService) InitSchedule(ctx context.Context) error { app.GetScheduler().MustAdd("cleanupCertificateExpired", "0 0 * * *", func() { s.cleanupExpiredCertificates(context.Background()) }) return nil } func (s *CertificateService) DownloadCertificate(ctx context.Context, req *dtos.CertificateDownloadReq) (*dtos.CertificateDownloadResp, error) { certificate, err := s.certificateRepo.GetById(ctx, req.CertificateId) if err != nil { return nil, err } canonicalName := strings.Split(certificate.SubjectAltNames, ";")[0] canonicalName = strings.ReplaceAll(canonicalName, "*", "_") var buf bytes.Buffer zipWriter := zip.NewWriter(&buf) defer zipWriter.Close() var bytes []byte switch strings.ToUpper(req.CertificateFormat) { case "", string(domain.CertificateFormatTypePEM): { serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certificate.Certificate) if err != nil { return nil, fmt.Errorf("failed to extract certs: %w", err) } certWriter, err := zipWriter.Create(fmt.Sprintf("%s.pem", canonicalName)) if err != nil { return nil, err } else { _, err = certWriter.Write([]byte(certificate.Certificate)) if err != nil { return nil, err } } serverCertWriter, err := zipWriter.Create(fmt.Sprintf("%s (server).pem", canonicalName)) if err != nil { return nil, err } else { _, err = serverCertWriter.Write([]byte(serverCertPEM)) if err != nil { return nil, err } } intermediaCertWriter, err := zipWriter.Create(fmt.Sprintf("%s (intermedia).pem", canonicalName)) if err != nil { return nil, err } else { _, err = intermediaCertWriter.Write([]byte(intermediaCertPEM)) if err != nil { return nil, err } } keyWriter, err := zipWriter.Create(fmt.Sprintf("%s.key", canonicalName)) if err != nil { return nil, err } else { _, err = keyWriter.Write([]byte(certificate.PrivateKey)) if err != nil { return nil, err } } err = zipWriter.Close() if err != nil { return nil, err } bytes = buf.Bytes() } case string(domain.CertificateFormatTypePFX): { const pfxPassword = "certimate" certPFX, err := xcert.TransformCertificateFromPEMToPFX(certificate.Certificate, certificate.PrivateKey, pfxPassword) if err != nil { return nil, err } certWriter, err := zipWriter.Create(fmt.Sprintf("%s.pfx", canonicalName)) if err != nil { return nil, err } else { _, err = certWriter.Write(certPFX) if err != nil { return nil, err } } keyWriter, err := zipWriter.Create("pfx-password.txt") if err != nil { return nil, err } else { _, err = keyWriter.Write([]byte(pfxPassword)) if err != nil { return nil, err } } err = zipWriter.Close() if err != nil { return nil, err } bytes = buf.Bytes() } case string(domain.CertificateFormatTypeJKS): { const jksPassword = "certimate" certJKS, err := xcert.TransformCertificateFromPEMToJKS(certificate.Certificate, certificate.PrivateKey, jksPassword, jksPassword, jksPassword) if err != nil { return nil, err } certWriter, err := zipWriter.Create(fmt.Sprintf("%s.jks", canonicalName)) if err != nil { return nil, err } else { _, err = certWriter.Write(certJKS) if err != nil { return nil, err } } keyWriter, err := zipWriter.Create("jks-password.txt") if err != nil { return nil, err } else { _, err = keyWriter.Write([]byte(jksPassword)) if err != nil { return nil, err } } err = zipWriter.Close() if err != nil { return nil, err } bytes = buf.Bytes() } default: return nil, domain.ErrInvalidParams } resp := &dtos.CertificateDownloadResp{ FileFormat: "zip", FileBytes: bytes, } return resp, nil } func (s *CertificateService) RevokeCertificate(ctx context.Context, req *dtos.CertificateRevokeReq) (*dtos.CertificateRevokeResp, error) { certificate, err := s.certificateRepo.GetById(ctx, req.CertificateId) if err != nil { return nil, err } if certificate.ACMEAcctUrl == "" || certificate.ACMECertUrl == "" { return nil, fmt.Errorf("could not revoke a certificate which is not issued in Certimate") } if certificate.IsRevoked { return nil, fmt.Errorf("could not revoke a certificate which is already revoked") } acmeAccount, err := s.acmeAccountRepo.GetByAcctUrl(ctx, certificate.ACMEAcctUrl) if err != nil { return nil, fmt.Errorf("failed to revoke certificate: could not find acme account: %w", err) } legoClient, err := certacme.NewACMEClientWithAccount(acmeAccount) if err != nil { return nil, fmt.Errorf("failed to revoke certificate: could not initialize acme config: %w", err) } revokeReq := &certacme.RevokeCertificateRequest{ Certificate: certificate.Certificate, } _, err = legoClient.RevokeCertificate(ctx, revokeReq) if err != nil { return nil, fmt.Errorf("failed to revoke certificate: %w", err) } certificate.IsRevoked = true certificate, err = s.certificateRepo.Save(ctx, certificate) if err != nil { return nil, err } return &dtos.CertificateRevokeResp{}, nil } func (s *CertificateService) cleanupExpiredCertificates(ctx context.Context) error { settings, err := s.settingsRepo.GetByName(ctx, domain.SettingsNamePersistence) if err != nil { if errors.Is(err, domain.ErrRecordNotFound) { return nil } app.GetLogger().Error("failed to get persistence settings", slog.Any("error", err)) return err } persistenceSettings := settings.Content.AsPersistence() if persistenceSettings.CertificatesRetentionMaxDays != 0 { ret, err := s.certificateRepo.DeleteWhere( context.Background(), dbx.NewExp(fmt.Sprintf("validityNotAfter 0 { app.GetLogger().Info(fmt.Sprintf("cleanup %d expired certificates", ret)) } } return nil } ================================================ FILE: internal/certificate/service_deps.go ================================================ package certificate import ( "context" "github.com/pocketbase/dbx" "github.com/certimate-go/certimate/internal/domain" ) type acmeAccountRepository interface { GetByAcctUrl(ctx context.Context, acctUrl string) (*domain.ACMEAccount, error) } type certificateRepository interface { ListExpiringSoon(ctx context.Context) ([]*domain.Certificate, error) GetById(ctx context.Context, id string) (*domain.Certificate, error) Save(ctx context.Context, certificate *domain.Certificate) (*domain.Certificate, error) DeleteWhere(ctx context.Context, exprs ...dbx.Expression) (int, error) } type settingsRepository interface { GetByName(ctx context.Context, name string) (*domain.Settings, error) } ================================================ FILE: internal/certmgmt/client.go ================================================ package certmgmt import ( "log/slog" ) type Client struct { logger *slog.Logger } type ClientConfigure func(*Client) func NewClient(configures ...ClientConfigure) *Client { client := &Client{} for _, configure := range configures { configure(client) } return client } func WithLogger(logger *slog.Logger) ClientConfigure { return func(c *Client) { c.logger = logger } } ================================================ FILE: internal/certmgmt/client_deploy.go ================================================ package certmgmt import ( "context" "errors" "fmt" "github.com/certimate-go/certimate/internal/certmgmt/deployers" "github.com/certimate-go/certimate/internal/domain" ) type DeployCertificateRequest struct { // 提供商相关 Provider string ProviderAccessConfig map[string]any ProviderExtendedConfig map[string]any // 证书相关 Certificate string PrivateKey string } type DeployCertificateResponse struct{} func (c *Client) DeployCertificate(ctx context.Context, request *DeployCertificateRequest) (*DeployCertificateResponse, error) { if request == nil { return nil, errors.New("the request is nil") } providerFactory, err := deployers.Registries.Get(domain.DeploymentProviderType(request.Provider)) if err != nil { return nil, err } provider, err := providerFactory(&deployers.ProviderFactoryOptions{ ProviderAccessConfig: request.ProviderAccessConfig, ProviderExtendedConfig: request.ProviderExtendedConfig, }) if err != nil { return nil, fmt.Errorf("failed to initialize deployment provider '%s': %w", request.Provider, err) } provider.SetLogger(c.logger) if _, err := provider.Deploy(ctx, request.Certificate, request.PrivateKey); err != nil { return nil, err } return &DeployCertificateResponse{}, nil } ================================================ FILE: internal/certmgmt/deployers/registry.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" ) type ProviderFactoryFunc func(options *ProviderFactoryOptions) (deployer.Provider, error) type ProviderFactoryOptions struct { ProviderAccessConfig map[string]any ProviderExtendedConfig map[string]any } type Registry[T comparable] interface { Register(T, ProviderFactoryFunc) error MustRegister(T, ProviderFactoryFunc) Get(T) (ProviderFactoryFunc, error) } type registry[T comparable] struct { factories map[T]ProviderFactoryFunc } func (r *registry[T]) Register(name T, factory ProviderFactoryFunc) error { if _, exists := r.factories[name]; exists { return fmt.Errorf("provider '%v' already registered", name) } r.factories[name] = factory return nil } func (r *registry[T]) MustRegister(name T, factory ProviderFactoryFunc) { if err := r.Register(name, factory); err != nil { panic(err) } } func (r *registry[T]) Get(name T) (ProviderFactoryFunc, error) { if factory, exists := r.factories[name]; exists { return factory, nil } return nil, fmt.Errorf("provider '%v' not registered", name) } func newRegistry[T comparable]() Registry[T] { return ®istry[T]{factories: make(map[T]ProviderFactoryFunc)} } var Registries = newRegistry[domain.DeploymentProviderType]() ================================================ FILE: internal/certmgmt/deployers/sp_1panel.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" onepanel "github.com/certimate-go/certimate/pkg/core/deployer/providers/1panel" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderType1Panel, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigFor1Panel{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := onepanel.NewDeployer(&onepanel.DeployerConfig{ ServerUrl: credentials.ServerUrl, ApiVersion: credentials.ApiVersion, ApiKey: credentials.ApiKey, AllowInsecureConnections: credentials.AllowInsecureConnections, NodeName: xmaps.GetString(options.ProviderExtendedConfig, "nodeName"), ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), WebsiteMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "websiteMatchPattern"), WebsiteId: xmaps.GetInt64(options.ProviderExtendedConfig, "websiteId"), CertificateId: xmaps.GetInt64(options.ProviderExtendedConfig, "certificateId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_1panel_console.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" opconsole "github.com/certimate-go/certimate/pkg/core/deployer/providers/1panel-console" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderType1PanelConsole, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigFor1Panel{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := opconsole.NewDeployer(&opconsole.DeployerConfig{ ServerUrl: credentials.ServerUrl, ApiVersion: credentials.ApiVersion, ApiKey: credentials.ApiKey, AllowInsecureConnections: credentials.AllowInsecureConnections, AutoRestart: xmaps.GetBool(options.ProviderExtendedConfig, "autoRestart"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_aliyun_alb.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" aliyunalb "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-alb" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeAliyunALB, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForAliyun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := aliyunalb.NewDeployer(&aliyunalb.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, ResourceGroupId: credentials.ResourceGroupId, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), LoadbalancerId: xmaps.GetString(options.ProviderExtendedConfig, "loadbalancerId"), ListenerId: xmaps.GetString(options.ProviderExtendedConfig, "listenerId"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_aliyun_apigw.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" aliyunapigw "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-apigw" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeAliyunAPIGW, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForAliyun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := aliyunapigw.NewDeployer(&aliyunapigw.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, ResourceGroupId: credentials.ResourceGroupId, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), ServiceType: xmaps.GetString(options.ProviderExtendedConfig, "serviceType"), GatewayId: xmaps.GetString(options.ProviderExtendedConfig, "gatewayId"), GroupId: xmaps.GetString(options.ProviderExtendedConfig, "groupId"), DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_aliyun_cas.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" aliyuncas "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-cas" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeAliyunCAS, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForAliyun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := aliyuncas.NewDeployer(&aliyuncas.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, ResourceGroupId: credentials.ResourceGroupId, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_aliyun_casdeploy.go ================================================ package deployers import ( "fmt" "strings" "github.com/samber/lo" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" aliyuncasdeploy "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-cas-deploy" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeAliyunCASDeploy, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForAliyun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := aliyuncasdeploy.NewDeployer(&aliyuncasdeploy.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, ResourceGroupId: credentials.ResourceGroupId, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), ResourceIds: lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, "resourceIds"), ";"), func(s string, _ int) bool { return s != "" }), ContactIds: lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, "contactIds"), ";"), func(s string, _ int) bool { return s != "" }), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_aliyun_cdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" aliyuncdn "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-cdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeAliyunCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForAliyun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := aliyuncdn.NewDeployer(&aliyuncdn.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, ResourceGroupId: credentials.ResourceGroupId, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_aliyun_clb.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" aliyunclb "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-clb" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeAliyunCLB, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForAliyun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := aliyunclb.NewDeployer(&aliyunclb.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, ResourceGroupId: credentials.ResourceGroupId, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), LoadbalancerId: xmaps.GetString(options.ProviderExtendedConfig, "loadbalancerId"), ListenerPort: xmaps.GetOrDefaultInt32(options.ProviderExtendedConfig, "listenerPort", 443), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_aliyun_dcdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" aliyundcdn "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-dcdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeAliyunDCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForAliyun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := aliyundcdn.NewDeployer(&aliyundcdn.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, ResourceGroupId: credentials.ResourceGroupId, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_aliyun_ddospro.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" aliyunddospro "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-ddospro" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeAliyunDDoSPro, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForAliyun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := aliyunddospro.NewDeployer(&aliyunddospro.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, ResourceGroupId: credentials.ResourceGroupId, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_aliyun_esa.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" aliyunesa "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-esa" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeAliyunESA, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForAliyun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := aliyunesa.NewDeployer(&aliyunesa.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), SiteId: xmaps.GetInt64(options.ProviderExtendedConfig, "siteId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_aliyun_esasaas.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" aliyunesasaas "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-esa-saas" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeAliyunESASaaS, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForAliyun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := aliyunesasaas.NewDeployer(&aliyunesasaas.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), SiteId: xmaps.GetInt64(options.ProviderExtendedConfig, "siteId"), DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_aliyun_fc.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" aliyunfc "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-fc" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeAliyunFC, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForAliyun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := aliyunfc.NewDeployer(&aliyunfc.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, ResourceGroupId: credentials.ResourceGroupId, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), ServiceVersion: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "serviceVersion", "3.0"), DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_aliyun_ga.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" aliyunga "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-ga" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeAliyunGA, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForAliyun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := aliyunga.NewDeployer(&aliyunga.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, ResourceGroupId: credentials.ResourceGroupId, ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), AcceleratorId: xmaps.GetString(options.ProviderExtendedConfig, "acceleratorId"), ListenerId: xmaps.GetString(options.ProviderExtendedConfig, "listenerId"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_aliyun_live.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" aliyunlive "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-live" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeAliyunLive, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForAliyun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := aliyunlive.NewDeployer(&aliyunlive.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_aliyun_nlb.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" aliyunnlb "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-nlb" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeAliyunNLB, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForAliyun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := aliyunnlb.NewDeployer(&aliyunnlb.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, ResourceGroupId: credentials.ResourceGroupId, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), LoadbalancerId: xmaps.GetString(options.ProviderExtendedConfig, "loadbalancerId"), ListenerId: xmaps.GetString(options.ProviderExtendedConfig, "listenerId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_aliyun_oss.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" aliyunoss "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-oss" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeAliyunOSS, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForAliyun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := aliyunoss.NewDeployer(&aliyunoss.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, ResourceGroupId: credentials.ResourceGroupId, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), Bucket: xmaps.GetString(options.ProviderExtendedConfig, "bucket"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_aliyun_vod.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" aliyunvod "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-vod" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeAliyunVOD, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForAliyun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := aliyunvod.NewDeployer(&aliyunvod.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, ResourceGroupId: credentials.ResourceGroupId, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_aliyun_waf.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" aliyunwaf "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-waf" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeAliyunWAF, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForAliyun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := aliyunwaf.NewDeployer(&aliyunwaf.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, ResourceGroupId: credentials.ResourceGroupId, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), ServiceVersion: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "serviceVersion", "3.0"), ServiceType: xmaps.GetString(options.ProviderExtendedConfig, "serviceType"), InstanceId: xmaps.GetString(options.ProviderExtendedConfig, "instanceId"), ResourceProduct: xmaps.GetString(options.ProviderExtendedConfig, "resourceProduct"), ResourceId: xmaps.GetString(options.ProviderExtendedConfig, "resourceId"), ResourcePort: xmaps.GetOrDefaultInt32(options.ProviderExtendedConfig, "resourcePort", 443), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_apisix.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/apisix" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeAPISIX, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForAPISIX{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := apisix.NewDeployer(&apisix.DeployerConfig{ ServerUrl: credentials.ServerUrl, ApiKey: credentials.ApiKey, AllowInsecureConnections: credentials.AllowInsecureConnections, ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), CertificateId: xmaps.GetString(options.ProviderExtendedConfig, "certificateId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_aws_acm.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" awsacm "github.com/certimate-go/certimate/pkg/core/deployer/providers/aws-acm" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeAWSACM, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForAWS{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := awsacm.NewDeployer(&awsacm.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), CertificateArn: xmaps.GetString(options.ProviderExtendedConfig, "certificateArn"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_aws_cloudfront.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" awscloudfront "github.com/certimate-go/certimate/pkg/core/deployer/providers/aws-cloudfront" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeAWSCloudFront, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForAWS{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := awscloudfront.NewDeployer(&awscloudfront.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), DistributionId: xmaps.GetString(options.ProviderExtendedConfig, "distributionId"), CertificateSource: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "certificateSource", "ACM"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_aws_iam.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" awsiam "github.com/certimate-go/certimate/pkg/core/deployer/providers/aws-iam" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeAWSIAM, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForAWS{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := awsiam.NewDeployer(&awsiam.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), CertificatePath: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "certificatePath", "/"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_azure_keyvault.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" azurekeyvault "github.com/certimate-go/certimate/pkg/core/deployer/providers/azure-keyvault" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeAzureKeyVault, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForAzure{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := azurekeyvault.NewDeployer(&azurekeyvault.DeployerConfig{ TenantId: credentials.TenantId, ClientId: credentials.ClientId, ClientSecret: credentials.ClientSecret, CloudName: credentials.CloudName, KeyVaultName: xmaps.GetString(options.ProviderExtendedConfig, "keyvaultName"), CertificateName: xmaps.GetString(options.ProviderExtendedConfig, "certificateName"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_baiducloud_appblb.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" baiducloudappblb "github.com/certimate-go/certimate/pkg/core/deployer/providers/baiducloud-appblb" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeBaiduCloudAppBLB, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForBaiduCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := baiducloudappblb.NewDeployer(&baiducloudappblb.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), LoadbalancerId: xmaps.GetString(options.ProviderExtendedConfig, "loadbalancerId"), ListenerPort: xmaps.GetInt32(options.ProviderExtendedConfig, "listenerPort"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_baiducloud_blb.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" baiducloudblb "github.com/certimate-go/certimate/pkg/core/deployer/providers/baiducloud-blb" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeBaiduCloudBLB, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForBaiduCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := baiducloudblb.NewDeployer(&baiducloudblb.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), LoadbalancerId: xmaps.GetString(options.ProviderExtendedConfig, "loadbalancerId"), ListenerPort: xmaps.GetInt32(options.ProviderExtendedConfig, "listenerPort"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_baiducloud_cdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" baiducloudcdn "github.com/certimate-go/certimate/pkg/core/deployer/providers/baiducloud-cdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeBaiduCloudCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForBaiduCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := baiducloudcdn.NewDeployer(&baiducloudcdn.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_baiducloud_cert.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" baiducloudcert "github.com/certimate-go/certimate/pkg/core/deployer/providers/baiducloud-cert" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeBaiduCloudCert, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForBaiduCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := baiducloudcert.NewDeployer(&baiducloudcert.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_baishan_cdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" baishancdn "github.com/certimate-go/certimate/pkg/core/deployer/providers/baishan-cdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeBaishanCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForBaishan{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := baishancdn.NewDeployer(&baishancdn.DeployerConfig{ ApiToken: credentials.ApiToken, ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), CertificateId: xmaps.GetString(options.ProviderExtendedConfig, "certificateId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_baotapanel.go ================================================ package deployers import ( "fmt" "strings" "github.com/samber/lo" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" baotapanel "github.com/certimate-go/certimate/pkg/core/deployer/providers/baotapanel" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeBaotaPanel, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForBaotaPanel{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := baotapanel.NewDeployer(&baotapanel.DeployerConfig{ ServerUrl: credentials.ServerUrl, ApiKey: credentials.ApiKey, AllowInsecureConnections: credentials.AllowInsecureConnections, SiteType: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "siteType", "other"), SiteNames: lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, "siteNames"), ";"), func(s string, _ int) bool { return s != "" }), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_baotapanel_console.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" baotapanelconsole "github.com/certimate-go/certimate/pkg/core/deployer/providers/baotapanel-console" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeBaotaPanelConsole, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForBaotaPanel{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := baotapanelconsole.NewDeployer(&baotapanelconsole.DeployerConfig{ ServerUrl: credentials.ServerUrl, ApiKey: credentials.ApiKey, AllowInsecureConnections: credentials.AllowInsecureConnections, AutoRestart: xmaps.GetBool(options.ProviderExtendedConfig, "autoRestart"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_baotapanelgo.go ================================================ package deployers import ( "fmt" "strings" "github.com/samber/lo" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" baotapanelgo "github.com/certimate-go/certimate/pkg/core/deployer/providers/baotapanelgo" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeBaotaPanelGo, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForBaotaPanelGo{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := baotapanelgo.NewDeployer(&baotapanelgo.DeployerConfig{ ServerUrl: credentials.ServerUrl, ApiKey: credentials.ApiKey, AllowInsecureConnections: credentials.AllowInsecureConnections, SiteType: xmaps.GetString(options.ProviderExtendedConfig, "siteType"), SiteNames: lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, "siteNames"), ";"), func(s string, _ int) bool { return s != "" }), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_baotapanelgo_console.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" baotapanelgoconsole "github.com/certimate-go/certimate/pkg/core/deployer/providers/baotapanelgo-console" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeBaotaPanelGoConsole, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForBaotaPanelGo{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := baotapanelgoconsole.NewDeployer(&baotapanelgoconsole.DeployerConfig{ ServerUrl: credentials.ServerUrl, ApiKey: credentials.ApiKey, AllowInsecureConnections: credentials.AllowInsecureConnections, }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_baotawaf.go ================================================ package deployers import ( "fmt" "strings" "github.com/samber/lo" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" baotawaf "github.com/certimate-go/certimate/pkg/core/deployer/providers/baotawaf" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeBaotaWAF, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForBaotaWAF{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := baotawaf.NewDeployer(&baotawaf.DeployerConfig{ ServerUrl: credentials.ServerUrl, ApiKey: credentials.ApiKey, AllowInsecureConnections: credentials.AllowInsecureConnections, SiteNames: lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, "siteNames"), ";"), func(s string, _ int) bool { return s != "" }), SitePort: xmaps.GetOrDefaultInt32(options.ProviderExtendedConfig, "sitePort", 443), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_baotawaf_console.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" baotawafconsole "github.com/certimate-go/certimate/pkg/core/deployer/providers/baotawaf-console" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeBaotaWAFConsole, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForBaotaWAF{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := baotawafconsole.NewDeployer(&baotawafconsole.DeployerConfig{ ServerUrl: credentials.ServerUrl, ApiKey: credentials.ApiKey, AllowInsecureConnections: credentials.AllowInsecureConnections, }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_bunny_cdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" bunnycdn "github.com/certimate-go/certimate/pkg/core/deployer/providers/bunny-cdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeBunnyCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForBunny{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := bunnycdn.NewDeployer(&bunnycdn.DeployerConfig{ ApiKey: credentials.ApiKey, PullZoneId: xmaps.GetString(options.ProviderExtendedConfig, "pullZoneId"), Hostname: xmaps.GetString(options.ProviderExtendedConfig, "hostname"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_byteplus_cdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" bytepluscdn "github.com/certimate-go/certimate/pkg/core/deployer/providers/byteplus-cdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeBytePlusCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForBytePlus{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := bytepluscdn.NewDeployer(&bytepluscdn.DeployerConfig{ AccessKey: credentials.AccessKey, SecretKey: credentials.SecretKey, DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_cachefly.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/cachefly" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeCacheFly, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForCacheFly{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := cachefly.NewDeployer(&cachefly.DeployerConfig{ ApiToken: credentials.ApiToken, }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_cdnfly.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/cdnfly" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeCdnfly, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForCdnfly{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } deployer, err := cdnfly.NewDeployer(&cdnfly.DeployerConfig{ ServerUrl: credentials.ServerUrl, ApiKey: credentials.ApiKey, ApiSecret: credentials.ApiSecret, AllowInsecureConnections: credentials.AllowInsecureConnections, ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), SiteId: xmaps.GetString(options.ProviderExtendedConfig, "siteId"), CertificateId: xmaps.GetString(options.ProviderExtendedConfig, "certificateId"), }) return deployer, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_cpanel.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/cpanel" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeCPanel, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForCPanel{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := cpanel.NewDeployer(&cpanel.DeployerConfig{ ServerUrl: credentials.ServerUrl, Username: credentials.Username, ApiToken: credentials.ApiToken, AllowInsecureConnections: credentials.AllowInsecureConnections, ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_ctcccloud_ao.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" ctcccloudao "github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-ao" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeCTCCCloudAO, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForCTCCCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := ctcccloudao.NewDeployer(&ctcccloudao.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_ctcccloud_cdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" ctcccloudcdn "github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-cdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeCTCCCloudCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForCTCCCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := ctcccloudcdn.NewDeployer(&ctcccloudcdn.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_ctcccloud_cms.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" ctcccloudcms "github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-cms" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeCTCCCloudCMS, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForCTCCCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := ctcccloudcms.NewDeployer(&ctcccloudcms.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_ctcccloud_elb.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" ctcccloudelb "github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-elb" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeCTCCCloudELB, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForCTCCCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := ctcccloudelb.NewDeployer(&ctcccloudelb.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, RegionId: xmaps.GetString(options.ProviderExtendedConfig, "regionId"), ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), LoadbalancerId: xmaps.GetString(options.ProviderExtendedConfig, "loadbalancerId"), ListenerId: xmaps.GetString(options.ProviderExtendedConfig, "listenerId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_ctcccloud_faas.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" ctcccloudfaas "github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-faas" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeCTCCCloudFaaS, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForCTCCCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := ctcccloudfaas.NewDeployer(&ctcccloudfaas.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, RegionId: xmaps.GetString(options.ProviderExtendedConfig, "regionId"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_ctcccloud_icdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" ctcccloudicdn "github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-icdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeCTCCCloudICDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForCTCCCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := ctcccloudicdn.NewDeployer(&ctcccloudicdn.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_ctcccloud_lvdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" ctcccloudlvdn "github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-lvdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeCTCCCloudLVDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForCTCCCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := ctcccloudlvdn.NewDeployer(&ctcccloudlvdn.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_dogecloud_cdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" pDogeCDN "github.com/certimate-go/certimate/pkg/core/deployer/providers/dogecloud-cdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeDogeCloudCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForDogeCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := pDogeCDN.NewDeployer(&pDogeCDN.DeployerConfig{ AccessKey: credentials.AccessKey, SecretKey: credentials.SecretKey, DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_dokploy.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/dokploy" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeDokploy, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForDokploy{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := dokploy.NewDeployer(&dokploy.DeployerConfig{ ServerUrl: credentials.ServerUrl, ApiKey: credentials.ApiKey, AllowInsecureConnections: credentials.AllowInsecureConnections, }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_flexcdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/flexcdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeFlexCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForFlexCDN{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := flexcdn.NewDeployer(&flexcdn.DeployerConfig{ ServerUrl: credentials.ServerUrl, ApiRole: credentials.ApiRole, AccessKeyId: credentials.AccessKeyId, AccessKey: credentials.AccessKey, AllowInsecureConnections: credentials.AllowInsecureConnections, ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), CertificateId: xmaps.GetInt64(options.ProviderExtendedConfig, "certificateId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_flyio.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" flyio "github.com/certimate-go/certimate/pkg/core/deployer/providers/flyio" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeFlyIO, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForFlyIO{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := flyio.NewDeployer(&flyio.DeployerConfig{ ApiToken: credentials.ApiToken, AppName: xmaps.GetString(options.ProviderExtendedConfig, "appName"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_gcore_cdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" gcorecdn "github.com/certimate-go/certimate/pkg/core/deployer/providers/gcore-cdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeGcoreCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForGcore{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := gcorecdn.NewDeployer(&gcorecdn.DeployerConfig{ ApiToken: credentials.ApiToken, ResourceId: xmaps.GetInt64(options.ProviderExtendedConfig, "resourceId"), CertificateId: xmaps.GetInt64(options.ProviderExtendedConfig, "certificateId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_goedge.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/goedge" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeGoEdge, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForGoEdge{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := goedge.NewDeployer(&goedge.DeployerConfig{ ServerUrl: credentials.ServerUrl, ApiRole: credentials.ApiRole, AccessKeyId: credentials.AccessKeyId, AccessKey: credentials.AccessKey, AllowInsecureConnections: credentials.AllowInsecureConnections, ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), CertificateId: xmaps.GetInt64(options.ProviderExtendedConfig, "certificateId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_huaweicloud_cdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" huaweicloudcdn "github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-cdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeHuaweiCloudCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForHuaweiCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := huaweicloudcdn.NewDeployer(&huaweicloudcdn.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, EnterpriseProjectId: credentials.EnterpriseProjectId, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_huaweicloud_elb.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" huaweicloudelb "github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-elb" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeHuaweiCloudELB, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForHuaweiCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := huaweicloudelb.NewDeployer(&huaweicloudelb.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, EnterpriseProjectId: credentials.EnterpriseProjectId, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), CertificateId: xmaps.GetString(options.ProviderExtendedConfig, "certificateId"), LoadbalancerId: xmaps.GetString(options.ProviderExtendedConfig, "loadbalancerId"), ListenerId: xmaps.GetString(options.ProviderExtendedConfig, "listenerId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_huaweicloud_obs.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" huaweicloudobs "github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-obs" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeHuaweiCloudOBS, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForHuaweiCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := huaweicloudobs.NewDeployer(&huaweicloudobs.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), Bucket: xmaps.GetString(options.ProviderExtendedConfig, "bucket"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_huaweicloud_scm.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" huaweicloudscm "github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-scm" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeHuaweiCloudSCM, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForHuaweiCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := huaweicloudscm.NewDeployer(&huaweicloudscm.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, EnterpriseProjectId: credentials.EnterpriseProjectId, }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_huaweicloud_waf.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" huaweicloudwaf "github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-waf" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeHuaweiCloudWAF, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForHuaweiCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := huaweicloudwaf.NewDeployer(&huaweicloudwaf.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, EnterpriseProjectId: credentials.EnterpriseProjectId, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), CertificateId: xmaps.GetString(options.ProviderExtendedConfig, "certificateId"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_jdcloud_alb.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" jdcloudalb "github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-alb" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeJDCloudALB, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForJDCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := jdcloudalb.NewDeployer(&jdcloudalb.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, RegionId: xmaps.GetString(options.ProviderExtendedConfig, "regionId"), ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), LoadbalancerId: xmaps.GetString(options.ProviderExtendedConfig, "loadbalancerId"), ListenerId: xmaps.GetString(options.ProviderExtendedConfig, "listenerId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_jdcloud_cdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" jdcloudcdn "github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-cdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeJDCloudCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForJDCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := jdcloudcdn.NewDeployer(&jdcloudcdn.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_jdcloud_live.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" jdcloudlive "github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-live" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeJDCloudLive, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForJDCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := jdcloudlive.NewDeployer(&jdcloudlive.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_jdcloud_vod.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" jdcloudvod "github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-vod" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeJDCloudVOD, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForJDCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := jdcloudvod.NewDeployer(&jdcloudvod.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_kong.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/kong" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeKong, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForKong{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := kong.NewDeployer(&kong.DeployerConfig{ ServerUrl: credentials.ServerUrl, ApiToken: credentials.ApiToken, AllowInsecureConnections: credentials.AllowInsecureConnections, ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), Workspace: xmaps.GetString(options.ProviderExtendedConfig, "workspace"), CertificateId: xmaps.GetString(options.ProviderExtendedConfig, "certificateId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_ksyun_cdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" ksyuncdn "github.com/certimate-go/certimate/pkg/core/deployer/providers/ksyun-cdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeKsyunCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForKsyun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := ksyuncdn.NewDeployer(&ksyuncdn.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, SecretAccessKey: credentials.SecretAccessKey, ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), CertificateId: xmaps.GetString(options.ProviderExtendedConfig, "certificateId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_kubernetes_secret.go ================================================ package deployers import ( "fmt" "strings" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" k8ssecret "github.com/certimate-go/certimate/pkg/core/deployer/providers/k8s-secret" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeKubernetesSecret, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForKubernetes{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } parseKeyValueMap := func(s string) (map[string]string, error) { result := make(map[string]string) lines := strings.Split(s, "\n") for i, line := range lines { if strings.TrimSpace(line) == "" { continue } pos := strings.Index(line, ":") if pos == -1 { return nil, fmt.Errorf("invalid line format at line %d", i+1) } key := strings.TrimSpace(line[:pos]) value := strings.TrimSpace(line[pos+1:]) if key == "" { return nil, fmt.Errorf("invalid key at line %d", i+1) } result[key] = value } return result, nil } secretAnnotations := make(map[string]string) if secretAnnotationsString := xmaps.GetString(options.ProviderExtendedConfig, "secretAnnotations"); secretAnnotationsString != "" { temp, err := parseKeyValueMap(secretAnnotationsString) if err != nil { return nil, fmt.Errorf("failed to parse kubernetes secret annotations: %w", err) } secretAnnotations = temp } secretLabels := make(map[string]string) if secretLabelsString := xmaps.GetString(options.ProviderExtendedConfig, "secretLabels"); secretLabelsString != "" { temp, err := parseKeyValueMap(secretLabelsString) if err != nil { return nil, fmt.Errorf("failed to parse kubernetes secret labels: %w", err) } secretLabels = temp } provider, err := k8ssecret.NewDeployer(&k8ssecret.DeployerConfig{ KubeConfig: credentials.KubeConfig, Namespace: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "namespace", "default"), SecretName: xmaps.GetString(options.ProviderExtendedConfig, "secretName"), SecretType: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "secretType", "kubernetes.io/tls"), SecretDataKeyForCrt: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "secretDataKeyForCrt", "tls.crt"), SecretDataKeyForKey: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "secretDataKeyForKey", "tls.key"), SecretAnnotations: secretAnnotations, SecretLabels: secretLabels, }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_lecdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/lecdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeLeCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForLeCDN{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := lecdn.NewDeployer(&lecdn.DeployerConfig{ ServerUrl: credentials.ServerUrl, ApiVersion: credentials.ApiVersion, ApiRole: credentials.ApiRole, Username: credentials.Username, Password: credentials.Password, AllowInsecureConnections: credentials.AllowInsecureConnections, ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), CertificateId: xmaps.GetInt64(options.ProviderExtendedConfig, "certificateId"), ClientId: xmaps.GetInt64(options.ProviderExtendedConfig, "clientId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_local.go ================================================ package deployers import ( "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/local" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeLocal, func(options *ProviderFactoryOptions) (deployer.Provider, error) { provider, err := local.NewDeployer(&local.DeployerConfig{ ShellEnv: xmaps.GetString(options.ProviderExtendedConfig, "shellEnv"), PreCommand: xmaps.GetString(options.ProviderExtendedConfig, "preCommand"), PostCommand: xmaps.GetString(options.ProviderExtendedConfig, "postCommand"), OutputFormat: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "format", local.OUTPUT_FORMAT_PEM), OutputCertPath: xmaps.GetString(options.ProviderExtendedConfig, "certPath"), OutputServerCertPath: xmaps.GetString(options.ProviderExtendedConfig, "certPathForServerOnly"), OutputIntermediaCertPath: xmaps.GetString(options.ProviderExtendedConfig, "certPathForIntermediaOnly"), OutputKeyPath: xmaps.GetString(options.ProviderExtendedConfig, "keyPath"), PfxPassword: xmaps.GetString(options.ProviderExtendedConfig, "pfxPassword"), JksAlias: xmaps.GetString(options.ProviderExtendedConfig, "jksAlias"), JksKeypass: xmaps.GetString(options.ProviderExtendedConfig, "jksKeypass"), JksStorepass: xmaps.GetString(options.ProviderExtendedConfig, "jksStorepass"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_mohua_mvh.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" mohuamvh "github.com/certimate-go/certimate/pkg/core/deployer/providers/mohua-mvh" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeMohuaMVH, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForMohua{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := mohuamvh.NewDeployer(&mohuamvh.DeployerConfig{ Username: credentials.Username, ApiPassword: credentials.ApiPassword, HostId: xmaps.GetString(options.ProviderExtendedConfig, "hostId"), DomainId: xmaps.GetString(options.ProviderExtendedConfig, "domainId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_netlify.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" netlify "github.com/certimate-go/certimate/pkg/core/deployer/providers/netlify" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeNetlify, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForNetlify{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := netlify.NewDeployer(&netlify.DeployerConfig{ ApiToken: credentials.ApiToken, ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), SiteId: xmaps.GetString(options.ProviderExtendedConfig, "siteId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_nginxproxymanager.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" nginxproxymanager "github.com/certimate-go/certimate/pkg/core/deployer/providers/nginxproxymanager" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeNginxProxyManager, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForNginxProxyManager{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := nginxproxymanager.NewDeployer(&nginxproxymanager.DeployerConfig{ ServerUrl: credentials.ServerUrl, AuthMethod: credentials.AuthMethod, Username: credentials.Username, Password: credentials.Password, ApiToken: credentials.ApiToken, AllowInsecureConnections: credentials.AllowInsecureConnections, ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), HostType: xmaps.GetString(options.ProviderExtendedConfig, "hostType"), HostMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "hostMatchPattern"), HostId: xmaps.GetInt64(options.ProviderExtendedConfig, "hostId"), CertificateId: xmaps.GetInt64(options.ProviderExtendedConfig, "certificateId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_proxmoxve.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/proxmoxve" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeProxmoxVE, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForProxmoxVE{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := proxmoxve.NewDeployer(&proxmoxve.DeployerConfig{ ServerUrl: credentials.ServerUrl, ApiToken: credentials.ApiToken, ApiTokenSecret: credentials.ApiTokenSecret, AllowInsecureConnections: credentials.AllowInsecureConnections, NodeName: xmaps.GetString(options.ProviderExtendedConfig, "nodeName"), AutoRestart: xmaps.GetBool(options.ProviderExtendedConfig, "autoRestart"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_qiniu_cdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" qiniucdn "github.com/certimate-go/certimate/pkg/core/deployer/providers/qiniu-cdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeQiniuCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForQiniu{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := qiniucdn.NewDeployer(&qiniucdn.DeployerConfig{ AccessKey: credentials.AccessKey, SecretKey: credentials.SecretKey, DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_qiniu_kodo.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" qiniukodo "github.com/certimate-go/certimate/pkg/core/deployer/providers/qiniu-kodo" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeQiniuKodo, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForQiniu{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := qiniukodo.NewDeployer(&qiniukodo.DeployerConfig{ AccessKey: credentials.AccessKey, SecretKey: credentials.SecretKey, Bucket: xmaps.GetString(options.ProviderExtendedConfig, "bucket"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_qiniu_pili.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" qiniupili "github.com/certimate-go/certimate/pkg/core/deployer/providers/qiniu-pili" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeQiniuPili, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForQiniu{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := qiniupili.NewDeployer(&qiniupili.DeployerConfig{ AccessKey: credentials.AccessKey, SecretKey: credentials.SecretKey, Hub: xmaps.GetString(options.ProviderExtendedConfig, "hub"), DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_rainyun_rcdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" rainyunrcdn "github.com/certimate-go/certimate/pkg/core/deployer/providers/rainyun-rcdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeRainYunRCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForRainYun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := rainyunrcdn.NewDeployer(&rainyunrcdn.DeployerConfig{ ApiKey: credentials.ApiKey, InstanceId: xmaps.GetInt64(options.ProviderExtendedConfig, "instanceId"), DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_rainyun_sslcenter.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" rainyunsslcenter "github.com/certimate-go/certimate/pkg/core/deployer/providers/rainyun-sslcenter" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeRainYunSSLCenter, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForRainYun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := rainyunsslcenter.NewDeployer(&rainyunsslcenter.DeployerConfig{ ApiKey: credentials.ApiKey, CertificateId: xmaps.GetInt64(options.ProviderExtendedConfig, "certificateId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_ratpanel.go ================================================ package deployers import ( "fmt" "strings" "github.com/samber/lo" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" ratpanel "github.com/certimate-go/certimate/pkg/core/deployer/providers/ratpanel" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeRatPanel, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForRatPanel{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := ratpanel.NewDeployer(&ratpanel.DeployerConfig{ ServerUrl: credentials.ServerUrl, AccessTokenId: credentials.AccessTokenId, AccessToken: credentials.AccessToken, AllowInsecureConnections: credentials.AllowInsecureConnections, ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), SiteNames: lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, "siteNames"), ";"), func(s string, _ int) bool { return s != "" }), CertificateId: xmaps.GetInt64(options.ProviderExtendedConfig, "certificateId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_ratpanel_console.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" ratpanelconsole "github.com/certimate-go/certimate/pkg/core/deployer/providers/ratpanel-console" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeRatPanelConsole, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForRatPanel{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := ratpanelconsole.NewDeployer(&ratpanelconsole.DeployerConfig{ ServerUrl: credentials.ServerUrl, AccessTokenId: credentials.AccessTokenId, AccessToken: credentials.AccessToken, AllowInsecureConnections: credentials.AllowInsecureConnections, }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_s3.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/s3" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeS3, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForS3{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := s3.NewDeployer(&s3.DeployerConfig{ Endpoint: credentials.Endpoint, AccessKey: credentials.AccessKey, SecretKey: credentials.SecretKey, SignatureVersion: credentials.SignatureVersion, UsePathStyle: credentials.UsePathStyle, AllowInsecureConnections: credentials.AllowInsecureConnections, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), Bucket: xmaps.GetString(options.ProviderExtendedConfig, "bucket"), OutputFormat: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "format", s3.OUTPUT_FORMAT_PEM), OutputCertObjectKey: xmaps.GetString(options.ProviderExtendedConfig, "certObjectKey"), OutputServerCertObjectKey: xmaps.GetString(options.ProviderExtendedConfig, "certObjectKeyForServerOnly"), OutputIntermediaCertObjectKey: xmaps.GetString(options.ProviderExtendedConfig, "certObjectKeyForIntermediaOnly"), OutputKeyObjectKey: xmaps.GetString(options.ProviderExtendedConfig, "keyObjectKey"), PfxPassword: xmaps.GetString(options.ProviderExtendedConfig, "pfxPassword"), JksAlias: xmaps.GetString(options.ProviderExtendedConfig, "jksAlias"), JksKeypass: xmaps.GetString(options.ProviderExtendedConfig, "jksKeypass"), JksStorepass: xmaps.GetString(options.ProviderExtendedConfig, "jksStorepass"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_safeline.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/safeline" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeSafeLine, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForSafeLine{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := safeline.NewDeployer(&safeline.DeployerConfig{ ServerUrl: credentials.ServerUrl, ApiToken: credentials.ApiToken, AllowInsecureConnections: credentials.AllowInsecureConnections, ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), CertificateId: xmaps.GetInt64(options.ProviderExtendedConfig, "certificateId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_ssh.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/ssh" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeSSH, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForSSH{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } jumpServers := make([]ssh.ServerConfig, len(credentials.JumpServers)) for i, jumpServer := range credentials.JumpServers { jumpServers[i] = ssh.ServerConfig{ SshHost: jumpServer.Host, SshPort: jumpServer.Port, SshAuthMethod: jumpServer.AuthMethod, SshUsername: jumpServer.Username, SshPassword: jumpServer.Password, SshKey: jumpServer.Key, SshKeyPassphrase: jumpServer.KeyPassphrase, } } provider, err := ssh.NewDeployer(&ssh.DeployerConfig{ ServerConfig: ssh.ServerConfig{ SshHost: credentials.Host, SshPort: credentials.Port, SshAuthMethod: credentials.AuthMethod, SshUsername: credentials.Username, SshPassword: credentials.Password, SshKey: credentials.Key, SshKeyPassphrase: credentials.KeyPassphrase, }, JumpServers: jumpServers, UseSCP: xmaps.GetBool(options.ProviderExtendedConfig, "useSCP"), PreCommand: xmaps.GetString(options.ProviderExtendedConfig, "preCommand"), PostCommand: xmaps.GetString(options.ProviderExtendedConfig, "postCommand"), OutputFormat: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "format", ssh.OUTPUT_FORMAT_PEM), OutputKeyPath: xmaps.GetString(options.ProviderExtendedConfig, "keyPath"), OutputCertPath: xmaps.GetString(options.ProviderExtendedConfig, "certPath"), OutputServerCertPath: xmaps.GetString(options.ProviderExtendedConfig, "certPathForServerOnly"), OutputIntermediaCertPath: xmaps.GetString(options.ProviderExtendedConfig, "certPathForIntermediaOnly"), PfxPassword: xmaps.GetString(options.ProviderExtendedConfig, "pfxPassword"), JksAlias: xmaps.GetString(options.ProviderExtendedConfig, "jksAlias"), JksKeypass: xmaps.GetString(options.ProviderExtendedConfig, "jksKeypass"), JksStorepass: xmaps.GetString(options.ProviderExtendedConfig, "jksStorepass"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_synologydsm.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/synologydsm" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeSynologyDSM, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForSynologyDSM{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := synologydsm.NewDeployer(&synologydsm.DeployerConfig{ ServerUrl: credentials.ServerUrl, Username: credentials.Username, Password: credentials.Password, TotpSecret: credentials.TotpSecret, AllowInsecureConnections: credentials.AllowInsecureConnections, CertificateIdOrDescription: xmaps.GetString(options.ProviderExtendedConfig, "certificateIdOrDesc"), IsDefault: xmaps.GetBool(options.ProviderExtendedConfig, "isDefault"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_tencentcloud_cdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" tencentcloudcdn "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-cdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeTencentCloudCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForTencentCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := tencentcloudcdn.NewDeployer(&tencentcloudcdn.DeployerConfig{ SecretId: credentials.SecretId, SecretKey: credentials.SecretKey, Endpoint: xmaps.GetString(options.ProviderExtendedConfig, "endpoint"), DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_tencentcloud_clb.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" tencentcloudclb "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-clb" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeTencentCloudCLB, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForTencentCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := tencentcloudclb.NewDeployer(&tencentcloudclb.DeployerConfig{ SecretId: credentials.SecretId, SecretKey: credentials.SecretKey, Endpoint: xmaps.GetString(options.ProviderExtendedConfig, "endpoint"), Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), LoadbalancerId: xmaps.GetString(options.ProviderExtendedConfig, "loadbalancerId"), ListenerId: xmaps.GetString(options.ProviderExtendedConfig, "listenerId"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_tencentcloud_cos.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" tencentcloudcos "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-cos" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeTencentCloudCOS, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForTencentCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := tencentcloudcos.NewDeployer(&tencentcloudcos.DeployerConfig{ SecretId: credentials.SecretId, SecretKey: credentials.SecretKey, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), Bucket: xmaps.GetString(options.ProviderExtendedConfig, "bucket"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_tencentcloud_css.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" tencentcloudcss "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-css" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeTencentCloudCSS, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForTencentCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := tencentcloudcss.NewDeployer(&tencentcloudcss.DeployerConfig{ SecretId: credentials.SecretId, SecretKey: credentials.SecretKey, Endpoint: xmaps.GetString(options.ProviderExtendedConfig, "endpoint"), DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_tencentcloud_ecdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" tencentcloudecdn "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-ecdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeTencentCloudECDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForTencentCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := tencentcloudecdn.NewDeployer(&tencentcloudecdn.DeployerConfig{ SecretId: credentials.SecretId, SecretKey: credentials.SecretKey, Endpoint: xmaps.GetString(options.ProviderExtendedConfig, "endpoint"), DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_tencentcloud_eo.go ================================================ package deployers import ( "fmt" "strings" "github.com/samber/lo" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" tencentcloudeo "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-eo" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeTencentCloudEO, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForTencentCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := tencentcloudeo.NewDeployer(&tencentcloudeo.DeployerConfig{ SecretId: credentials.SecretId, SecretKey: credentials.SecretKey, Endpoint: xmaps.GetString(options.ProviderExtendedConfig, "endpoint"), ZoneId: xmaps.GetString(options.ProviderExtendedConfig, "zoneId"), DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domains: lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, "domains"), ";"), func(s string, _ int) bool { return s != "" }), EnableMultipleSSL: xmaps.GetBool(options.ProviderExtendedConfig, "enableMultipleSSL"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_tencentcloud_gaap.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" tencentcloudgaap "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-gaap" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeTencentCloudGAAP, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForTencentCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := tencentcloudgaap.NewDeployer(&tencentcloudgaap.DeployerConfig{ SecretId: credentials.SecretId, SecretKey: credentials.SecretKey, Endpoint: xmaps.GetString(options.ProviderExtendedConfig, "endpoint"), ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), ProxyId: xmaps.GetString(options.ProviderExtendedConfig, "proxyId"), ListenerId: xmaps.GetString(options.ProviderExtendedConfig, "listenerId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_tencentcloud_scf.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" tencentcloudscf "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-scf" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeTencentCloudSCF, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForTencentCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := tencentcloudscf.NewDeployer(&tencentcloudscf.DeployerConfig{ SecretId: credentials.SecretId, SecretKey: credentials.SecretKey, Endpoint: xmaps.GetString(options.ProviderExtendedConfig, "endpoint"), Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_tencentcloud_ssl.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" tencentcloudssl "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-ssl" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeTencentCloudSSL, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForTencentCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := tencentcloudssl.NewDeployer(&tencentcloudssl.DeployerConfig{ SecretId: credentials.SecretId, SecretKey: credentials.SecretKey, Endpoint: xmaps.GetString(options.ProviderExtendedConfig, "endpoint"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_tencentcloud_ssldeploy.go ================================================ package deployers import ( "fmt" "strings" "github.com/samber/lo" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" tencentcloudssldeploy "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-ssl-deploy" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeTencentCloudSSLDeploy, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForTencentCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := tencentcloudssldeploy.NewDeployer(&tencentcloudssldeploy.DeployerConfig{ SecretId: credentials.SecretId, SecretKey: credentials.SecretKey, Endpoint: xmaps.GetString(options.ProviderExtendedConfig, "endpoint"), Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), ResourceProduct: xmaps.GetString(options.ProviderExtendedConfig, "resourceProduct"), ResourceIds: lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, "resourceIds"), ";"), func(s string, _ int) bool { return s != "" }), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_tencentcloud_sslupdate.go ================================================ package deployers import ( "fmt" "strings" "github.com/samber/lo" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" tencentcloudsslupdate "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-ssl-update" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeTencentCloudSSLUpdate, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForTencentCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := tencentcloudsslupdate.NewDeployer(&tencentcloudsslupdate.DeployerConfig{ SecretId: credentials.SecretId, SecretKey: credentials.SecretKey, Endpoint: xmaps.GetString(options.ProviderExtendedConfig, "endpoint"), CertificateId: xmaps.GetString(options.ProviderExtendedConfig, "certificateId"), IsReplaced: xmaps.GetBool(options.ProviderExtendedConfig, "isReplaced"), ResourceProducts: lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, "resourceProducts"), ";"), func(s string, _ int) bool { return s != "" }), ResourceRegions: lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, "resourceRegions"), ";"), func(s string, _ int) bool { return s != "" }), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_tencentcloud_vod.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" tencentcloudvod "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-vod" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeTencentCloudVOD, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForTencentCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := tencentcloudvod.NewDeployer(&tencentcloudvod.DeployerConfig{ SecretId: credentials.SecretId, SecretKey: credentials.SecretKey, Endpoint: xmaps.GetString(options.ProviderExtendedConfig, "endpoint"), SubAppId: xmaps.GetInt64(options.ProviderExtendedConfig, "subAppId"), DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_tencentcloud_waf.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" tencentcloudwaf "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-waf" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeTencentCloudWAF, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForTencentCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := tencentcloudwaf.NewDeployer(&tencentcloudwaf.DeployerConfig{ SecretId: credentials.SecretId, SecretKey: credentials.SecretKey, Endpoint: xmaps.GetString(options.ProviderExtendedConfig, "endpoint"), Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), DomainId: xmaps.GetString(options.ProviderExtendedConfig, "domainId"), InstanceId: xmaps.GetString(options.ProviderExtendedConfig, "instanceId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_ucloud_ualb.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" ucloudualb "github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-ualb" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeUCloudUALB, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForUCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := ucloudualb.NewDeployer(&ucloudualb.DeployerConfig{ PrivateKey: credentials.PrivateKey, PublicKey: credentials.PublicKey, ProjectId: credentials.ProjectId, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), LoadbalancerId: xmaps.GetString(options.ProviderExtendedConfig, "loadbalancerId"), ListenerId: xmaps.GetString(options.ProviderExtendedConfig, "listenerId"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_ucloud_ucdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" uclouducdn "github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-ucdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeUCloudUCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForUCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := uclouducdn.NewDeployer(&uclouducdn.DeployerConfig{ PrivateKey: credentials.PrivateKey, PublicKey: credentials.PublicKey, ProjectId: credentials.ProjectId, DomainId: xmaps.GetString(options.ProviderExtendedConfig, "domainId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_ucloud_uclb.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" uclouduclb "github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-uclb" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeUCloudUCLB, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForUCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := uclouduclb.NewDeployer(&uclouduclb.DeployerConfig{ PrivateKey: credentials.PrivateKey, PublicKey: credentials.PublicKey, ProjectId: credentials.ProjectId, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), LoadbalancerId: xmaps.GetString(options.ProviderExtendedConfig, "loadbalancerId"), VServerId: xmaps.GetString(options.ProviderExtendedConfig, "vserverId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_ucloud_uewaf.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" uclouduewaf "github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-uewaf" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeUCloudUEWAF, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForUCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := uclouduewaf.NewDeployer(&uclouduewaf.DeployerConfig{ PrivateKey: credentials.PrivateKey, PublicKey: credentials.PublicKey, ProjectId: credentials.ProjectId, Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_ucloud_upathx.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" ucloudupathx "github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-upathx" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeUCloudUPathX, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForUCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := ucloudupathx.NewDeployer(&ucloudupathx.DeployerConfig{ PrivateKey: credentials.PrivateKey, PublicKey: credentials.PublicKey, ProjectId: credentials.ProjectId, AcceleratorId: xmaps.GetString(options.ProviderExtendedConfig, "acceleratorId"), ListenerPort: xmaps.GetInt32(options.ProviderExtendedConfig, "listenerPort"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_ucloud_us3.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" ucloudus3 "github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-us3" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeUCloudUS3, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForUCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := ucloudus3.NewDeployer(&ucloudus3.DeployerConfig{ PrivateKey: credentials.PrivateKey, PublicKey: credentials.PublicKey, ProjectId: credentials.ProjectId, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), Bucket: xmaps.GetString(options.ProviderExtendedConfig, "bucket"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_unicloud_webhost.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" unicloudwebhost "github.com/certimate-go/certimate/pkg/core/deployer/providers/unicloud-webhost" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeUniCloudWebHost, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForUniCloud{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := unicloudwebhost.NewDeployer(&unicloudwebhost.DeployerConfig{ Username: credentials.Username, Password: credentials.Password, SpaceProvider: xmaps.GetString(options.ProviderExtendedConfig, "spaceProvider"), SpaceId: xmaps.GetString(options.ProviderExtendedConfig, "spaceId"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_upyun_cdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" upyuncdn "github.com/certimate-go/certimate/pkg/core/deployer/providers/upyun-cdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeUpyunCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForUpyun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := upyuncdn.NewDeployer(&upyuncdn.DeployerConfig{ Username: credentials.Username, Password: credentials.Password, DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_upyun_file.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" upyunfile "github.com/certimate-go/certimate/pkg/core/deployer/providers/upyun-file" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeUpyunFile, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForUpyun{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := upyunfile.NewDeployer(&upyunfile.DeployerConfig{ Username: credentials.Username, Password: credentials.Password, Bucket: xmaps.GetString(options.ProviderExtendedConfig, "bucket"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_volcengine_alb.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" volcenginealb "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-alb" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeVolcEngineALB, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForVolcEngine{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := volcenginealb.NewDeployer(&volcenginealb.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.SecretAccessKey, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), LoadbalancerId: xmaps.GetString(options.ProviderExtendedConfig, "loadbalancerId"), ListenerId: xmaps.GetString(options.ProviderExtendedConfig, "listenerId"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_volcengine_cdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" volcenginecdn "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-cdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeVolcEngineCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForVolcEngine{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := volcenginecdn.NewDeployer(&volcenginecdn.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.SecretAccessKey, DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_volcengine_certcenter.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" volcenginecertcenter "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-certcenter" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeVolcEngineCertCenter, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForVolcEngine{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := volcenginecertcenter.NewDeployer(&volcenginecertcenter.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.SecretAccessKey, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_volcengine_clb.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" volcengineclb "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-clb" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeVolcEngineCLB, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForVolcEngine{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := volcengineclb.NewDeployer(&volcengineclb.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.SecretAccessKey, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), LoadbalancerId: xmaps.GetString(options.ProviderExtendedConfig, "loadbalancerId"), ListenerId: xmaps.GetString(options.ProviderExtendedConfig, "listenerId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_volcengine_dcdn.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" volcenginedcdn "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-dcdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeVolcEngineDCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForVolcEngine{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := volcenginedcdn.NewDeployer(&volcenginedcdn.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.SecretAccessKey, DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_volcengine_imagex.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" volcengineimagex "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-imagex" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeVolcEngineImageX, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForVolcEngine{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := volcengineimagex.NewDeployer(&volcengineimagex.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.SecretAccessKey, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), ServiceId: xmaps.GetString(options.ProviderExtendedConfig, "serviceId"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_volcengine_live.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" volcenginelive "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-live" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeVolcEngineLive, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForVolcEngine{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := volcenginelive.NewDeployer(&volcenginelive.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.SecretAccessKey, DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_volcengine_tos.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" volcenginetos "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-tos" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeVolcEngineTOS, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForVolcEngine{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := volcenginetos.NewDeployer(&volcenginetos.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.SecretAccessKey, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), Bucket: xmaps.GetString(options.ProviderExtendedConfig, "bucket"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_volcengine_vod.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" volcenginevod "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-vod" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeVolcEngineVOD, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForVolcEngine{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := volcenginevod.NewDeployer(&volcenginevod.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.SecretAccessKey, SpaceName: xmaps.GetString(options.ProviderExtendedConfig, "spaceName"), DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), DomainType: xmaps.GetString(options.ProviderExtendedConfig, "domainType"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_volcengine_waf.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" volcenginewaf "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-waf" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeVolcEngineWAF, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForVolcEngine{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := volcenginewaf.NewDeployer(&volcenginewaf.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.SecretAccessKey, Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), AccessMode: xmaps.GetString(options.ProviderExtendedConfig, "accessMode"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_wangsu_cdn.go ================================================ package deployers import ( "fmt" "strings" "github.com/samber/lo" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" wangsucdn "github.com/certimate-go/certimate/pkg/core/deployer/providers/wangsu-cdn" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeWangsuCDN, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForWangsu{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := wangsucdn.NewDeployer(&wangsucdn.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domains: lo.Filter(strings.Split(xmaps.GetString(options.ProviderExtendedConfig, "domains"), ";"), func(s string, _ int) bool { return s != "" }), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_wangsu_cdnpro.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" wangsucdnpro "github.com/certimate-go/certimate/pkg/core/deployer/providers/wangsu-cdnpro" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeWangsuCDNPro, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForWangsu{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := wangsucdnpro.NewDeployer(&wangsucdnpro.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, ApiKey: credentials.ApiKey, Environment: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "environment", "production"), DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), CertificateId: xmaps.GetString(options.ProviderExtendedConfig, "certificateId"), WebhookId: xmaps.GetString(options.ProviderExtendedConfig, "webhookId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_wangsu_certificate.go ================================================ package deployers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" wangsucertificate "github.com/certimate-go/certimate/pkg/core/deployer/providers/wangsu-certificate" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeWangsuCertificate, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForWangsu{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := wangsucertificate.NewDeployer(&wangsucertificate.DeployerConfig{ AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, CertificateId: xmaps.GetString(options.ProviderExtendedConfig, "certificateId"), }) return provider, err }) } ================================================ FILE: internal/certmgmt/deployers/sp_webhook.go ================================================ package deployers import ( "fmt" "net/http" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/deployer" webhook "github.com/certimate-go/certimate/pkg/core/deployer/providers/webhook" xhttp "github.com/certimate-go/certimate/pkg/utils/http" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.DeploymentProviderTypeWebhook, func(options *ProviderFactoryOptions) (deployer.Provider, error) { credentials := domain.AccessConfigForWebhook{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } mergedHeaders := make(map[string]string) if defaultHeadersString := credentials.HeadersString; defaultHeadersString != "" { h, err := xhttp.ParseHeaders(defaultHeadersString) if err != nil { return nil, fmt.Errorf("failed to parse webhook headers: %w", err) } for key := range h { mergedHeaders[http.CanonicalHeaderKey(key)] = h.Get(key) } } if extendedHeadersString := xmaps.GetString(options.ProviderExtendedConfig, "headers"); extendedHeadersString != "" { h, err := xhttp.ParseHeaders(extendedHeadersString) if err != nil { return nil, fmt.Errorf("failed to parse webhook headers: %w", err) } for key := range h { mergedHeaders[http.CanonicalHeaderKey(key)] = h.Get(key) } } provider, err := webhook.NewDeployer(&webhook.DeployerConfig{ WebhookUrl: credentials.Url, WebhookData: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "webhookData", credentials.DataString), Method: credentials.Method, Headers: mergedHeaders, Timeout: xmaps.GetInt(options.ProviderExtendedConfig, "timeout"), AllowInsecureConnections: credentials.AllowInsecureConnections, }) return provider, err }) } ================================================ FILE: internal/domain/access.go ================================================ package domain import "time" const CollectionNameAccess = "access" type Access struct { Meta Name string `db:"name" json:"name"` Provider string `db:"provider" json:"provider"` Config map[string]any `db:"config" json:"config"` Reserve string `db:"reserve" json:"reserve,omitempty"` DeletedAt *time.Time `db:"deleted" json:"deleted"` } type AccessConfigFor1Panel struct { ServerUrl string `json:"serverUrl"` ApiVersion string `json:"apiVersion"` ApiKey string `json:"apiKey"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type AccessConfigFor35cn struct { Username string `json:"username"` ApiPassword string `json:"apiPassword"` } type AccessConfigFor51DNScom struct { ApiKey string `json:"apiKey"` ApiSecret string `json:"apiSecret"` } type AccessConfigForACMEExternalAccountBinding struct { EabKid string `json:"eabKid,omitempty"` EabHmacKey string `json:"eabHmacKey,omitempty"` } type AccessConfigForACMECA struct { AccessConfigForACMEExternalAccountBinding Endpoint string `json:"endpoint"` } type AccessConfigForACMEDNS struct { ServerUrl string `json:"serverUrl"` Credentials string `json:"credentials"` } type AccessConfigForACMEHttpReq struct { Endpoint string `json:"endpoint"` Mode string `json:"mode,omitempty"` Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` } type AccessConfigForActalisSSL struct { AccessConfigForACMEExternalAccountBinding } type AccessConfigForAkamai struct { Host string `json:"host"` ClientToken string `json:"clientToken"` ClientSecret string `json:"clientSecret"` AccessToken string `json:"accessToken"` } type AccessConfigForAliyun struct { AccessKeyId string `json:"accessKeyId"` AccessKeySecret string `json:"accessKeySecret"` ResourceGroupId string `json:"resourceGroupId,omitempty"` } type AccessConfigForAPISIX struct { ServerUrl string `json:"serverUrl"` ApiKey string `json:"apiKey"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type AccessConfigForArvanCloud struct { ApiKey string `json:"apiKey"` } type AccessConfigForAWS struct { AccessKeyId string `json:"accessKeyId"` SecretAccessKey string `json:"secretAccessKey"` } type AccessConfigForAzure struct { TenantId string `json:"tenantId"` ClientId string `json:"clientId"` ClientSecret string `json:"clientSecret"` SubscriptionId string `json:"subscriptionId,omitempty"` ResourceGroupName string `json:"resourceGroupName,omitempty"` CloudName string `json:"cloudName,omitempty"` } type AccessConfigForBaiduCloud struct { AccessKeyId string `json:"accessKeyId"` SecretAccessKey string `json:"secretAccessKey"` } type AccessConfigForBaishan struct { ApiToken string `json:"apiToken"` } type AccessConfigForBaotaPanel struct { ServerUrl string `json:"serverUrl"` ApiKey string `json:"apiKey"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type AccessConfigForBaotaPanelGo struct { ServerUrl string `json:"serverUrl"` ApiKey string `json:"apiKey"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type AccessConfigForBaotaWAF struct { ServerUrl string `json:"serverUrl"` ApiKey string `json:"apiKey"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type AccessConfigForBookMyName struct { Username string `json:"username"` Password string `json:"password"` } type AccessConfigForBunny struct { ApiKey string `json:"apiKey"` } type AccessConfigForBytePlus struct { AccessKey string `json:"accessKey"` SecretKey string `json:"secretKey"` } type AccessConfigForCacheFly struct { ApiToken string `json:"apiToken"` } type AccessConfigForCdnfly struct { ServerUrl string `json:"serverUrl"` ApiKey string `json:"apiKey"` ApiSecret string `json:"apiSecret"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type AccessConfigForCloudflare struct { DnsApiToken string `json:"dnsApiToken"` ZoneApiToken string `json:"zoneApiToken,omitempty"` } type AccessConfigForClouDNS struct { AuthId string `json:"authId"` AuthPassword string `json:"authPassword"` } type AccessConfigForCMCCCloud struct { AccessKeyId string `json:"accessKeyId"` AccessKeySecret string `json:"accessKeySecret"` } type AccessConfigForConstellix struct { ApiKey string `json:"apiKey"` SecretKey string `json:"secretKey"` } type AccessConfigForCPanel struct { ServerUrl string `json:"serverUrl"` Username string `json:"username"` ApiToken string `json:"apiToken"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type AccessConfigForCTCCCloud struct { AccessKeyId string `json:"accessKeyId"` SecretAccessKey string `json:"secretAccessKey"` } type AccessConfigForDeSEC struct { Token string `json:"token"` } type AccessConfigForDigitalOcean struct { AccessToken string `json:"accessToken"` } type AccessConfigForDingTalkBot struct { WebhookUrl string `json:"webhookUrl"` Secret string `json:"secret,omitempty"` CustomPayload string `json:"customPayload,omitempty"` } type AccessConfigForDiscordBot struct { BotToken string `json:"botToken"` ChannelId string `json:"channelId,omitempty"` } type AccessConfigForDNSExit struct { ApiKey string `json:"apiKey"` } type AccessConfigForDNSLA struct { ApiId string `json:"apiId"` ApiSecret string `json:"apiSecret"` } type AccessConfigForDNSMadeEasy struct { ApiKey string `json:"apiKey"` ApiSecret string `json:"apiSecret"` } type AccessConfigForDogeCloud struct { AccessKey string `json:"accessKey"` SecretKey string `json:"secretKey"` } type AccessConfigForDokploy struct { ServerUrl string `json:"serverUrl"` ApiKey string `json:"apiKey"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type AccessConfigForDuckDNS struct { Token string `json:"token"` } type AccessConfigForDynu struct { ApiKey string `json:"apiKey"` } type AccessConfigForDynv6 struct { HttpToken string `json:"httpToken"` } type AccessConfigForEmail struct { SmtpHost string `json:"smtpHost"` SmtpPort int32 `json:"smtpPort"` SmtpTls bool `json:"smtpTls"` Username string `json:"username"` Password string `json:"password"` SenderAddress string `json:"senderAddress"` SenderName string `json:"senderName"` ReceiverAddress string `json:"receiverAddress,omitempty"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type AccessConfigForFlexCDN struct { ServerUrl string `json:"serverUrl"` ApiRole string `json:"apiRole"` AccessKeyId string `json:"accessKeyId"` AccessKey string `json:"accessKey"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type AccessConfigForFlyIO struct { ApiToken string `json:"apiToken"` } type AccessConfigForGandinet struct { PersonalAccessToken string `json:"personalAccessToken"` } type AccessConfigForGcore struct { ApiToken string `json:"apiToken"` } type AccessConfigForGlobalSectigo struct { AccessConfigForACMEExternalAccountBinding ValidationType string `json:"validationType"` } type AccessConfigForGlobalSignAtlas struct { AccessConfigForACMEExternalAccountBinding } type AccessConfigForGname struct { AppId string `json:"appId"` AppKey string `json:"appKey"` } type AccessConfigForGoDaddy struct { ApiKey string `json:"apiKey"` ApiSecret string `json:"apiSecret"` } type AccessConfigForGoEdge struct { ServerUrl string `json:"serverUrl"` ApiRole string `json:"apiRole"` AccessKeyId string `json:"accessKeyId"` AccessKey string `json:"accessKey"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type AccessConfigForGoogleTrustServices struct { AccessConfigForACMEExternalAccountBinding } type AccessConfigForHetzner struct { ApiToken string `json:"apiToken"` } type AccessConfigForHostingde struct { ApiKey string `json:"apiKey"` } type AccessConfigForHostinger struct { ApiToken string `json:"apiToken"` } type AccessConfigForHuaweiCloud struct { AccessKeyId string `json:"accessKeyId"` SecretAccessKey string `json:"secretAccessKey"` EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` } type AccessConfigForInfomaniak struct { AccessToken string `json:"accessToken"` } type AccessConfigForIONOS struct { ApiKeyPublicPrefix string `json:"apiKeyPublicPrefix"` ApiKeySecret string `json:"apiKeySecret"` } type AccessConfigForJDCloud struct { AccessKeyId string `json:"accessKeyId"` AccessKeySecret string `json:"accessKeySecret"` } type AccessConfigForKong struct { ServerUrl string `json:"serverUrl"` ApiToken string `json:"apiToken,omitempty"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type AccessConfigForKubernetes struct { KubeConfig string `json:"kubeConfig,omitempty"` } type AccessConfigForKsyun struct { AccessKeyId string `json:"accessKeyId"` SecretAccessKey string `json:"secretAccessKey"` } type AccessConfigForLarkBot struct { WebhookUrl string `json:"webhookUrl"` Secret string `json:"secret,omitempty"` CustomPayload string `json:"customPayload,omitempty"` } type AccessConfigForLeCDN struct { ServerUrl string `json:"serverUrl"` ApiVersion string `json:"apiVersion"` ApiRole string `json:"apiRole"` Username string `json:"username"` Password string `json:"password"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type AccessConfigForLinode struct { AccessToken string `json:"accessToken"` } type AccessConfigForLiteSSL struct { AccessConfigForACMEExternalAccountBinding } type AccessConfigForMattermost struct { ServerUrl string `json:"serverUrl"` Username string `json:"username"` Password string `json:"password"` ChannelId string `json:"channelId,omitempty"` } type AccessConfigForMohua struct { Username string `json:"username"` ApiPassword string `json:"apiPassword"` } type AccessConfigForNamecheap struct { Username string `json:"username"` ApiKey string `json:"apiKey"` } type AccessConfigForNameDotCom struct { Username string `json:"username"` ApiToken string `json:"apiToken"` } type AccessConfigForNameSilo struct { ApiKey string `json:"apiKey"` } type AccessConfigForNetcup struct { CustomerNumber string `json:"customerNumber"` ApiKey string `json:"apiKey"` ApiPassword string `json:"apiPassword"` } type AccessConfigForNetlify struct { ApiToken string `json:"apiToken"` } type AccessConfigForNginxProxyManager struct { ServerUrl string `json:"serverUrl"` AuthMethod string `json:"authMethod"` Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` ApiToken string `json:"apiToken,omitempty"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type AccessConfigForNS1 struct { ApiKey string `json:"apiKey"` } type AccessConfigForOVHcloud struct { Endpoint string `json:"endpoint"` AuthMethod string `json:"authMethod"` ApplicationKey string `json:"applicationKey,omitempty"` ApplicationSecret string `json:"applicationSecret,omitempty"` ConsumerKey string `json:"consumerKey,omitempty"` ClientId string `json:"clientId,omitempty"` ClientSecret string `json:"clientSecret,omitempty"` } type AccessConfigForPorkbun struct { ApiKey string `json:"apiKey"` SecretApiKey string `json:"secretApiKey"` } type AccessConfigForPowerDNS struct { ServerUrl string `json:"serverUrl"` ApiKey string `json:"apiKey"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type AccessConfigForProxmoxVE struct { ServerUrl string `json:"serverUrl"` ApiToken string `json:"apiToken"` ApiTokenSecret string `json:"apiTokenSecret,omitempty"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type AccessConfigForQingCloud struct { AccessKeyId string `json:"accessKeyId"` SecretAccessKey string `json:"secretAccessKey"` } type AccessConfigForQiniu struct { AccessKey string `json:"accessKey"` SecretKey string `json:"secretKey"` } type AccessConfigForRainYun struct { ApiKey string `json:"apiKey"` } type AccessConfigForRatPanel struct { ServerUrl string `json:"serverUrl"` AccessTokenId int64 `json:"accessTokenId"` AccessToken string `json:"accessToken"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type AccessConfigForRFC2136 struct { Host string `json:"host"` Port int32 `json:"port"` TsigAlgorithm string `json:"tsigAlgorithm,omitempty"` TsigKey string `json:"tsigKey,omitempty"` TsigSecret string `json:"tsigSecret,omitempty"` } type AccessConfigForS3 struct { Endpoint string `json:"endpoint"` AccessKey string `json:"accessKey"` SecretKey string `json:"secretKey"` SignatureVersion string `json:"signatureVersion,omitempty"` UsePathStyle bool `json:"usePathStyle,omitempty"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type AccessConfigForSafeLine struct { ServerUrl string `json:"serverUrl"` ApiToken string `json:"apiToken"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type AccessConfigForSlackBot struct { BotToken string `json:"botToken"` ChannelId string `json:"channelId,omitempty"` } type AccessConfigForSpaceship struct { ApiKey string `json:"apiKey"` ApiSecret string `json:"apiSecret"` } type AccessConfigForSSH struct { Host string `json:"host"` Port int32 `json:"port"` AuthMethod string `json:"authMethod"` Username string `json:"username"` Password string `json:"password,omitempty"` Key string `json:"key,omitempty"` KeyPassphrase string `json:"keyPassphrase,omitempty"` JumpServers []struct { Host string `json:"host"` Port int32 `json:"port"` AuthMethod string `json:"authMethod"` Username string `json:"username"` Password string `json:"password,omitempty"` Key string `json:"key,omitempty"` KeyPassphrase string `json:"keyPassphrase,omitempty"` } `json:"jumpServers,omitempty"` } type AccessConfigForSSLCom struct { AccessConfigForACMEExternalAccountBinding } type AccessConfigForSynologyDSM struct { ServerUrl string `json:"serverUrl"` Username string `json:"username"` Password string `json:"password"` TotpSecret string `json:"totpSecret,omitempty"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type AccessConfigForTechnitiumDNS struct { ServerUrl string `json:"serverUrl"` ApiToken string `json:"apiToken"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type AccessConfigForTelegramBot struct { BotToken string `json:"botToken"` ChatId string `json:"chatId,omitempty"` } type AccessConfigForTodayNIC struct { UserId string `json:"userId"` ApiKey string `json:"apiKey"` } type AccessConfigForTencentCloud struct { SecretId string `json:"secretId"` SecretKey string `json:"secretKey"` } type AccessConfigForUCloud struct { PrivateKey string `json:"privateKey"` PublicKey string `json:"publicKey"` ProjectId string `json:"projectId,omitempty"` } type AccessConfigForUniCloud struct { Username string `json:"username"` Password string `json:"password"` } type AccessConfigForUpyun struct { Username string `json:"username"` Password string `json:"password"` } type AccessConfigForVercel struct { ApiAccessToken string `json:"apiAccessToken"` TeamId string `json:"teamId,omitempty"` } type AccessConfigForVolcEngine struct { AccessKeyId string `json:"accessKeyId"` SecretAccessKey string `json:"secretAccessKey"` } type AccessConfigForVultr struct { ApiKey string `json:"apiKey"` } type AccessConfigForWangsu struct { AccessKeyId string `json:"accessKeyId"` AccessKeySecret string `json:"accessKeySecret"` ApiKey string `json:"apiKey"` } type AccessConfigForWebhook struct { Url string `json:"url"` Method string `json:"method,omitempty"` HeadersString string `json:"headers,omitempty"` DataString string `json:"data,omitempty"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type AccessConfigForWeComBot struct { WebhookUrl string `json:"webhookUrl"` CustomPayload string `json:"customPayload,omitempty"` } type AccessConfigForWestcn struct { Username string `json:"username"` ApiPassword string `json:"apiPassword"` } type AccessConfigForXinnet struct { AgentId string `json:"agentId"` ApiPassword string `json:"apiPassword"` } type AccessConfigForZeroSSL struct { AccessConfigForACMEExternalAccountBinding } ================================================ FILE: internal/domain/acme_account.go ================================================ package domain import ( "crypto" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/registration" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) const CollectionNameACMEAccount = "acme_accounts" type ACMEAccount struct { Meta CA string `db:"ca" json:"ca"` Email string `db:"email" json:"email"` PrivateKey string `db:"privateKey" json:"privateKey"` ACMEAccount *acme.Account `db:"acmeAccount" json:"acmeAccount"` ACMEAcctUrl string `db:"acmeAcctUrl" json:"acmeAcctUrl"` ACMEDirUrl string `db:"acmeDirUrl" json:"acmeDirUrl"` } func (a *ACMEAccount) GetEmail() string { return a.Email } func (a *ACMEAccount) GetRegistration() *registration.Resource { if a.ACMEAccount == nil { return nil } return ®istration.Resource{ Body: *a.ACMEAccount, URI: a.ACMEAcctUrl, } } func (a *ACMEAccount) GetPrivateKey() crypto.PrivateKey { if a.PrivateKey == "" { return nil } rs, _ := xcert.ParsePrivateKeyFromPEM(a.PrivateKey) return rs } ================================================ FILE: internal/domain/certificate.go ================================================ package domain import ( "crypto/x509" "fmt" "strings" "time" "github.com/go-acme/lego/v4/certcrypto" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcertkey "github.com/certimate-go/certimate/pkg/utils/cert/key" xcertx509 "github.com/certimate-go/certimate/pkg/utils/cert/x509" ) const CollectionNameCertificate = "certificate" type Certificate struct { Meta Source CertificateSourceType `db:"source" json:"source"` SubjectAltNames string `db:"subjectAltNames" json:"subjectAltNames"` SerialNumber string `db:"serialNumber" json:"serialNumber"` Certificate string `db:"certificate" json:"certificate"` PrivateKey string `db:"privateKey" json:"privateKey"` IssuerOrg string `db:"issuerOrg" json:"issuerOrg"` IssuerCertificate string `db:"issuerCertificate" json:"issuerCertificate"` KeyAlgorithm CertificateKeyAlgorithmType `db:"keyAlgorithm" json:"keyAlgorithm"` ValidityNotBefore time.Time `db:"validityNotBefore" json:"validityNotBefore"` ValidityNotAfter time.Time `db:"validityNotAfter" json:"validityNotAfter"` ValidityInterval int32 `db:"validityInterval" json:"validityInterval"` ACMEAcctUrl string `db:"acmeAcctUrl" json:"acmeAcctUrl"` ACMECertUrl string `db:"acmeCertUrl" json:"acmeCertUrl"` IsRenewed bool `db:"isRenewed" json:"isRenewed"` IsRevoked bool `db:"isRevoked" json:"isRevoked"` WorkflowId string `db:"workflowRef" json:"workflowId"` WorkflowRunId string `db:"workflowRunRef" json:"workflowRunId"` WorkflowNodeId string `db:"workflowNodeId" json:"workflowNodeId"` DeletedAt *time.Time `db:"deleted" json:"deleted"` } func (c *Certificate) PopulateFromX509(certX509 *x509.Certificate) *Certificate { c.SubjectAltNames = strings.Join(xcertx509.GetSubjectAltNames(certX509), ";") c.SerialNumber = strings.ToUpper(certX509.SerialNumber.Text(16)) c.IssuerOrg = strings.Join(certX509.Issuer.Organization, ";") c.ValidityNotBefore = certX509.NotBefore c.ValidityNotAfter = certX509.NotAfter c.ValidityInterval = int32(certX509.NotAfter.Sub(certX509.NotBefore).Seconds()) keyAlgorithm, keySize, _ := xcertkey.GetPublicKeyAlgorithm(certX509.PublicKey) switch keyAlgorithm { case x509.RSA: c.KeyAlgorithm = CertificateKeyAlgorithmType(fmt.Sprintf("RSA%d", keySize)) case x509.ECDSA: c.KeyAlgorithm = CertificateKeyAlgorithmType(fmt.Sprintf("EC%d", keySize)) case x509.Ed25519: c.KeyAlgorithm = CertificateKeyAlgorithmType("Ed25519") default: c.KeyAlgorithm = CertificateKeyAlgorithmType("") } return c } func (c *Certificate) PopulateFromPEM(certPEM, privkeyPEM string) *Certificate { c.Certificate = certPEM c.PrivateKey = privkeyPEM _, issuerCertPEM, _ := xcert.ExtractCertificatesFromPEM(certPEM) c.IssuerCertificate = issuerCertPEM certX509, _ := xcert.ParseCertificateFromPEM(certPEM) if certX509 != nil { return c.PopulateFromX509(certX509) } return c } type CertificateSourceType string const ( CertificateSourceTypeRequest = CertificateSourceType("request") 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") ) func (t CertificateKeyAlgorithmType) KeyType() (certcrypto.KeyType, error) { keyTypeMap := map[CertificateKeyAlgorithmType]certcrypto.KeyType{ CertificateKeyAlgorithmTypeRSA2048: certcrypto.RSA2048, CertificateKeyAlgorithmTypeRSA3072: certcrypto.RSA3072, CertificateKeyAlgorithmTypeRSA4096: certcrypto.RSA4096, CertificateKeyAlgorithmTypeRSA8192: certcrypto.RSA8192, CertificateKeyAlgorithmTypeEC256: certcrypto.EC256, CertificateKeyAlgorithmTypeEC384: certcrypto.EC384, } if keyType, ok := keyTypeMap[t]; ok { return keyType, nil } return certcrypto.RSA2048, fmt.Errorf("unsupported key algorithm type: '%s'", t) } type CertificateFormatType string const ( CertificateFormatTypePEM CertificateFormatType = "PEM" CertificateFormatTypePFX CertificateFormatType = "PFX" CertificateFormatTypeJKS CertificateFormatType = "JKS" ) ================================================ FILE: internal/domain/dtos/certificate.go ================================================ package dtos type CertificateDownloadReq struct { CertificateId string `json:"-"` CertificateFormat string `json:"format"` } type CertificateDownloadResp struct { FileBytes []byte `json:"fileBytes"` FileFormat string `json:"fileFormat"` } type CertificateRevokeReq struct { CertificateId string `json:"-"` } type CertificateRevokeResp struct{} ================================================ FILE: internal/domain/dtos/notify.go ================================================ package dtos type NotifyTestPushReq struct { Provider string `json:"provider"` AccessId string `json:"accessId"` } type NotifyTestPushResp struct{} ================================================ FILE: internal/domain/dtos/workflow.go ================================================ package dtos import "github.com/certimate-go/certimate/internal/domain" type WorkflowStartRunReq struct { WorkflowId string `json:"-"` RunTrigger domain.WorkflowTriggerType `json:"trigger"` } type WorkflowStartRunResp struct { RunId string `json:"runId"` } type WorkflowCancelRunReq struct { WorkflowId string `json:"-"` RunId string `json:"-"` } type WorkflowCancelRunResp struct{} type WorkflowStatisticsResp struct { Concurrency int `json:"concurrency"` PendingRunIds []string `json:"pendingRunIds"` ProcessingRunIds []string `json:"processingRunIds"` } ================================================ FILE: internal/domain/error.go ================================================ package domain var ( ErrInvalidParams = NewError(400, "invalid params") ErrRecordNotFound = NewError(404, "record not found") ) type Error struct { Code int `json:"code"` Msg string `json:"msg"` } func NewError(code int, msg string) *Error { if code == 0 { code = -1 } return &Error{code, msg} } func (e *Error) Error() string { return e.Msg } func IsRecordNotFoundError(err error) bool { if e, ok := err.(*Error); ok { return e.Code == ErrRecordNotFound.Code } return false } ================================================ FILE: internal/domain/expr/expr.go ================================================ package expr import ( "encoding/json" "fmt" "strconv" ) type ( ExprType string ExprComparisonOperator string ExprLogicalOperator string ExprValueType string ) const ( GreaterThan ExprComparisonOperator = "gt" GreaterOrEqual ExprComparisonOperator = "gte" LessThan ExprComparisonOperator = "lt" LessOrEqual ExprComparisonOperator = "lte" Equal ExprComparisonOperator = "eq" NotEqual ExprComparisonOperator = "neq" And ExprLogicalOperator = "and" Or ExprLogicalOperator = "or" Not ExprLogicalOperator = "not" Number ExprValueType = "number" String ExprValueType = "string" Boolean ExprValueType = "boolean" ConstantExprType ExprType = "const" VariantExprType ExprType = "var" ComparisonExprType ExprType = "comparison" LogicalExprType ExprType = "logical" NotExprType ExprType = "not" ) type EvalResult struct { Type ExprValueType Value any } func (e *EvalResult) GetFloat64() (float64, error) { if e.Type != Number { return 0, fmt.Errorf("type mismatch: %s", e.Type) } stringValue, ok := e.Value.(string) if !ok { return 0, fmt.Errorf("value is not a string: %v", e.Value) } floatValue, err := strconv.ParseFloat(stringValue, 64) if err != nil { return 0, fmt.Errorf("failed to parse float64: %w", err) } return floatValue, nil } func (e *EvalResult) GetBool() (bool, error) { if e.Type != Boolean { return false, fmt.Errorf("type mismatch: %s", e.Type) } strValue, ok := e.Value.(string) if ok { if strValue == "true" { return true, nil } else if strValue == "false" { return false, nil } return false, fmt.Errorf("value is not a boolean: %v", e.Value) } boolValue, ok := e.Value.(bool) if !ok { return false, fmt.Errorf("value is not a boolean: %v", e.Value) } return boolValue, nil } func (e *EvalResult) GreaterThan(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } switch e.Type { case String: return &EvalResult{ Type: Boolean, Value: e.Value.(string) > other.Value.(string), }, nil case Number: left, err := e.GetFloat64() if err != nil { return nil, err } right, err := other.GetFloat64() if err != nil { return nil, err } return &EvalResult{ Type: Boolean, Value: left > right, }, nil default: return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } func (e *EvalResult) GreaterOrEqual(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } switch e.Type { case String: return &EvalResult{ Type: Boolean, Value: e.Value.(string) >= other.Value.(string), }, nil case Number: left, err := e.GetFloat64() if err != nil { return nil, err } right, err := other.GetFloat64() if err != nil { return nil, err } return &EvalResult{ Type: Boolean, Value: left >= right, }, nil default: return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } func (e *EvalResult) LessThan(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } switch e.Type { case String: return &EvalResult{ Type: Boolean, Value: e.Value.(string) < other.Value.(string), }, nil case Number: left, err := e.GetFloat64() if err != nil { return nil, err } right, err := other.GetFloat64() if err != nil { return nil, err } return &EvalResult{ Type: Boolean, Value: left < right, }, nil default: return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } func (e *EvalResult) LessOrEqual(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } switch e.Type { case String: return &EvalResult{ Type: Boolean, Value: e.Value.(string) <= other.Value.(string), }, nil case Number: left, err := e.GetFloat64() if err != nil { return nil, err } right, err := other.GetFloat64() if err != nil { return nil, err } return &EvalResult{ Type: Boolean, Value: left <= right, }, nil default: return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } func (e *EvalResult) Equal(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } switch e.Type { case String: return &EvalResult{ Type: Boolean, Value: e.Value.(string) == other.Value.(string), }, nil case Number: left, err := e.GetFloat64() if err != nil { return nil, err } right, err := other.GetFloat64() if err != nil { return nil, err } return &EvalResult{ Type: Boolean, Value: left == right, }, nil case Boolean: left, err := e.GetBool() if err != nil { return nil, err } right, err := other.GetBool() if err != nil { return nil, err } return &EvalResult{ Type: Boolean, Value: left == right, }, nil default: return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } func (e *EvalResult) NotEqual(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } switch e.Type { case String: return &EvalResult{ Type: Boolean, Value: e.Value.(string) != other.Value.(string), }, nil case Number: left, err := e.GetFloat64() if err != nil { return nil, err } right, err := other.GetFloat64() if err != nil { return nil, err } return &EvalResult{ Type: Boolean, Value: left != right, }, nil case Boolean: left, err := e.GetBool() if err != nil { return nil, err } right, err := other.GetBool() if err != nil { return nil, err } return &EvalResult{ Type: Boolean, Value: left != right, }, nil default: return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } func (e *EvalResult) And(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } switch e.Type { case Boolean: left, err := e.GetBool() if err != nil { return nil, err } right, err := other.GetBool() if err != nil { return nil, err } return &EvalResult{ Type: Boolean, Value: left && right, }, nil default: return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } func (e *EvalResult) Or(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } switch e.Type { case Boolean: left, err := e.GetBool() if err != nil { return nil, err } right, err := other.GetBool() if err != nil { return nil, err } return &EvalResult{ Type: Boolean, Value: left || right, }, nil default: return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } func (e *EvalResult) Not() (*EvalResult, error) { if e.Type != Boolean { return nil, fmt.Errorf("type mismatch: %s", e.Type) } boolValue, err := e.GetBool() if err != nil { return nil, err } return &EvalResult{ Type: Boolean, Value: !boolValue, }, nil } type Expr interface { GetType() ExprType Eval(variables map[string]map[string]any) (*EvalResult, error) } type ExprValueSelector struct { Id string `json:"id"` Name string `json:"name"` Type ExprValueType `json:"type"` } type ConstantExpr struct { Type ExprType `json:"type"` Value string `json:"value"` ValueType ExprValueType `json:"valueType"` } func (c ConstantExpr) GetType() ExprType { return c.Type } func (c ConstantExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { return &EvalResult{ Type: c.ValueType, Value: c.Value, }, nil } type VariantExpr struct { Type ExprType `json:"type"` Selector ExprValueSelector `json:"selector"` } func (v VariantExpr) GetType() ExprType { return v.Type } func (v VariantExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { if v.Selector.Id == "" { return nil, fmt.Errorf("node id is empty") } if v.Selector.Name == "" { return nil, fmt.Errorf("name is empty") } if _, ok := variables[v.Selector.Id]; !ok { return nil, fmt.Errorf("node %s not found", v.Selector.Id) } if _, ok := variables[v.Selector.Id][v.Selector.Name]; !ok { return nil, fmt.Errorf("variable %s not found in node %s", v.Selector.Name, v.Selector.Id) } return &EvalResult{ Type: v.Selector.Type, Value: variables[v.Selector.Id][v.Selector.Name], }, nil } type ComparisonExpr struct { Type ExprType `json:"type"` // compare Operator ExprComparisonOperator `json:"operator"` Left Expr `json:"left"` Right Expr `json:"right"` } func (c ComparisonExpr) GetType() ExprType { return c.Type } func (c ComparisonExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { left, err := c.Left.Eval(variables) if err != nil { return nil, err } right, err := c.Right.Eval(variables) if err != nil { return nil, err } switch c.Operator { case GreaterThan: return left.GreaterThan(right) case LessThan: return left.LessThan(right) case GreaterOrEqual: return left.GreaterOrEqual(right) case LessOrEqual: return left.LessOrEqual(right) case Equal: return left.Equal(right) case NotEqual: return left.NotEqual(right) default: return nil, fmt.Errorf("unknown expression operator: %s", c.Operator) } } type LogicalExpr struct { Type ExprType `json:"type"` // logical Operator ExprLogicalOperator `json:"operator"` Left Expr `json:"left"` Right Expr `json:"right"` } func (l LogicalExpr) GetType() ExprType { return l.Type } func (l LogicalExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { left, err := l.Left.Eval(variables) if err != nil { return nil, err } right, err := l.Right.Eval(variables) if err != nil { return nil, err } switch l.Operator { case And: return left.And(right) case Or: return left.Or(right) default: return nil, fmt.Errorf("unknown expression operator: %s", l.Operator) } } type NotExpr struct { Type ExprType `json:"type"` // not Expr Expr `json:"expr"` } func (n NotExpr) GetType() ExprType { return n.Type } func (n NotExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { inner, err := n.Expr.Eval(variables) if err != nil { return nil, err } return inner.Not() } type rawExpr struct { Type ExprType `json:"type"` } func MarshalExpr(e Expr) ([]byte, error) { return json.Marshal(e) } func UnmarshalExpr(data []byte) (Expr, error) { var typ rawExpr if err := json.Unmarshal(data, &typ); err != nil { return nil, err } switch typ.Type { case ConstantExprType: var e ConstantExpr if err := json.Unmarshal(data, &e); err != nil { return nil, err } return e, nil case VariantExprType: var e VariantExpr if err := json.Unmarshal(data, &e); err != nil { return nil, err } return e, nil case ComparisonExprType: var e ComparisonExprRaw if err := json.Unmarshal(data, &e); err != nil { return nil, err } return e.ToComparisonExpr() case LogicalExprType: var e LogicalExprRaw if err := json.Unmarshal(data, &e); err != nil { return nil, err } return e.ToLogicalExpr() case NotExprType: var e NotExprRaw if err := json.Unmarshal(data, &e); err != nil { return nil, err } return e.ToNotExpr() default: return nil, fmt.Errorf("unknown expression type: %s", typ.Type) } } type ComparisonExprRaw struct { Type ExprType `json:"type"` Operator ExprComparisonOperator `json:"operator"` Left json.RawMessage `json:"left"` Right json.RawMessage `json:"right"` } func (r ComparisonExprRaw) ToComparisonExpr() (ComparisonExpr, error) { leftExpr, err := UnmarshalExpr(r.Left) if err != nil { return ComparisonExpr{}, err } rightExpr, err := UnmarshalExpr(r.Right) if err != nil { return ComparisonExpr{}, err } return ComparisonExpr{ Type: r.Type, Operator: r.Operator, Left: leftExpr, Right: rightExpr, }, nil } type LogicalExprRaw struct { Type ExprType `json:"type"` Operator ExprLogicalOperator `json:"operator"` Left json.RawMessage `json:"left"` Right json.RawMessage `json:"right"` } func (r LogicalExprRaw) ToLogicalExpr() (LogicalExpr, error) { left, err := UnmarshalExpr(r.Left) if err != nil { return LogicalExpr{}, err } right, err := UnmarshalExpr(r.Right) if err != nil { return LogicalExpr{}, err } return LogicalExpr{ Type: r.Type, Operator: r.Operator, Left: left, Right: right, }, nil } type NotExprRaw struct { Type ExprType `json:"type"` Expr json.RawMessage `json:"expr"` } func (r NotExprRaw) ToNotExpr() (NotExpr, error) { inner, err := UnmarshalExpr(r.Expr) if err != nil { return NotExpr{}, err } return NotExpr{ Type: r.Type, Expr: inner, }, nil } ================================================ FILE: internal/domain/expr/expr_test.go ================================================ package expr import ( "testing" ) func TestLogicalEval(t *testing.T) { // 测试逻辑表达式 and logicalExpr := LogicalExpr{ Left: ConstantExpr{ Type: "const", Value: "true", ValueType: "boolean", }, Operator: And, Right: ConstantExpr{ Type: "const", Value: "true", ValueType: "boolean", }, } result, err := logicalExpr.Eval(nil) if err != nil { t.Errorf("failed to evaluate logical expression: %v", err) } if result.Value != true { t.Errorf("expected true, got %v", result) } // 测试逻辑表达式 or orExpr := LogicalExpr{ Left: ConstantExpr{ Type: "const", Value: "true", ValueType: "boolean", }, Operator: Or, Right: ConstantExpr{ Type: "const", Value: "true", ValueType: "boolean", }, } result, err = orExpr.Eval(nil) if err != nil { t.Errorf("failed to evaluate logical expression: %v", err) } if result.Value != true { t.Errorf("expected true, got %v", result) } } func TestUnmarshalExpr(t *testing.T) { type args struct { data []byte } tests := []struct { name string args args want Expr wantErr bool }{ { name: "test1", args: args{ data: []byte(`{"left":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.validity","type":"boolean"},"type":"var"},"operator":"is","right":{"type":"const","value":true,"valueType":"boolean"},"type":"comparison"},"operator":"and","right":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.daysLeft","type":"number"},"type":"var"},"operator":"eq","right":{"type":"const","value":2,"valueType":"number"},"type":"comparison"},"type":"logical"}`), }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := UnmarshalExpr(tt.args.data) if (err != nil) != tt.wantErr { t.Errorf("UnmarshalExpr() error = %v, wantErr %v", err, tt.wantErr) return } if got == nil { t.Errorf("UnmarshalExpr() got = nil, want %v", tt.want) return } }) } } func TestExpr_Eval(t *testing.T) { type args struct { variables map[string]map[string]any data []byte } tests := []struct { name string args args want *EvalResult wantErr bool }{ { name: "test1", args: args{ variables: map[string]map[string]any{ "ODnYSOXB6HQP2_vz6JcZE": { "certificate.validity": true, "certificate.daysLeft": 2, }, }, data: []byte(`{"left":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.validity","type":"boolean"},"type":"var"},"operator":"is","right":{"type":"const","value":true,"valueType":"boolean"},"type":"comparison"},"operator":"and","right":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.daysLeft","type":"number"},"type":"var"},"operator":"eq","right":{"type":"const","value":2,"valueType":"number"},"type":"comparison"},"type":"logical"}`), }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c, err := UnmarshalExpr(tt.args.data) if err != nil { t.Errorf("UnmarshalExpr() error = %v", err) return } got, err := c.Eval(tt.args.variables) t.Log("got:", got) if (err != nil) != tt.wantErr { t.Errorf("ConstExpr.Eval() error = %v, wantErr %v", err, tt.wantErr) return } if got.Value != true { t.Errorf("ConstExpr.Eval() got = %v, want %v", got.Value, true) } }) } } ================================================ FILE: internal/domain/meta.go ================================================ package domain import "time" type Meta struct { Id string `db:"id" json:"id"` CreatedAt time.Time `db:"created" json:"created"` UpdatedAt time.Time `db:"updated" json:"updated"` } ================================================ FILE: internal/domain/provider.go ================================================ package domain type AccessProviderType string /* 授权提供商类型常量值。 注意:如果追加新的常量值,请保持以 ASCII 排序。 NOTICE: If you add new constant, please keep ASCII order. */ const ( AccessProviderType1Panel = AccessProviderType("1panel") AccessProviderType35cn = AccessProviderType("35cn") AccessProviderType51DNScom = AccessProviderType("51dnscom") AccessProviderTypeACMECA = AccessProviderType("acmeca") AccessProviderTypeACMEDNS = AccessProviderType("acmedns") AccessProviderTypeACMEHttpReq = AccessProviderType("acmehttpreq") AccessProviderTypeActalisSSL = AccessProviderType("actalisssl") AccessProviderTypeAkamai = AccessProviderType("akamai") AccessProviderTypeAliyun = AccessProviderType("aliyun") AccessProviderTypeAPISIX = AccessProviderType("apisix") AccessProviderTypeArvanCloud = AccessProviderType("arvancloud") AccessProviderTypeAWS = AccessProviderType("aws") AccessProviderTypeAzure = AccessProviderType("azure") AccessProviderTypeBaiduCloud = AccessProviderType("baiducloud") AccessProviderTypeBaishan = AccessProviderType("baishan") AccessProviderTypeBaotaPanel = AccessProviderType("baotapanel") AccessProviderTypeBaotaPanelGo = AccessProviderType("baotapanelgo") AccessProviderTypeBaotaWAF = AccessProviderType("baotawaf") AccessProviderTypeBookMyName = AccessProviderType("bookmyname") AccessProviderTypeBunny = AccessProviderType("bunny") AccessProviderTypeBytePlus = AccessProviderType("byteplus") AccessProviderTypeCacheFly = AccessProviderType("cachefly") AccessProviderTypeCdnfly = AccessProviderType("cdnfly") AccessProviderTypeCloudflare = AccessProviderType("cloudflare") AccessProviderTypeClouDNS = AccessProviderType("cloudns") AccessProviderTypeCMCCCloud = AccessProviderType("cmcccloud") AccessProviderTypeConstellix = AccessProviderType("constellix") AccessProviderTypeCPanel = AccessProviderType("cpanel") AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud") AccessProviderTypeCUCCCloud = AccessProviderType("cucccloud") // 联通云(预留) AccessProviderTypeDeSEC = AccessProviderType("desec") AccessProviderTypeDigiCert = AccessProviderType("digicert") AccessProviderTypeDigitalOcean = AccessProviderType("digitalocean") AccessProviderTypeDingTalkBot = AccessProviderType("dingtalkbot") AccessProviderTypeDiscordBot = AccessProviderType("discordbot") AccessProviderTypeDNSExit = AccessProviderType("dnsexit") AccessProviderTypeDNSLA = AccessProviderType("dnsla") AccessProviderTypeDNSMadeEasy = AccessProviderType("dnsmadeeasy") AccessProviderTypeDogeCloud = AccessProviderType("dogecloud") AccessProviderTypeDokploy = AccessProviderType("dokploy") AccessProviderTypeDuckDNS = AccessProviderType("duckdns") AccessProviderTypeDynu = AccessProviderType("dynu") AccessProviderTypeDynv6 = AccessProviderType("dynv6") AccessProviderTypeEmail = AccessProviderType("email") AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly(预留) AccessProviderTypeFlexCDN = AccessProviderType("flexcdn") AccessProviderTypeFlyIO = AccessProviderType("flyio") AccessProviderTypeGandinet = AccessProviderType("gandinet") AccessProviderTypeGcore = AccessProviderType("gcore") AccessProviderTypeGlobalSignAtlas = AccessProviderType("globalsignatlas") AccessProviderTypeGname = AccessProviderType("gname") AccessProviderTypeGoDaddy = AccessProviderType("godaddy") AccessProviderTypeGoEdge = AccessProviderType("goedge") AccessProviderTypeGoogleTrustServices = AccessProviderType("googletrustservices") AccessProviderTypeHetzner = AccessProviderType("hetzner") AccessProviderTypeHostingde = AccessProviderType("hostingde") AccessProviderTypeHostinger = AccessProviderType("hostinger") AccessProviderTypeHuaweiCloud = AccessProviderType("huaweicloud") AccessProviderTypeInfomaniak = AccessProviderType("infomaniak") AccessProviderTypeIONOS = AccessProviderType("ionos") AccessProviderTypeJDCloud = AccessProviderType("jdcloud") AccessProviderTypeKong = AccessProviderType("kong") AccessProviderTypeKsyun = AccessProviderType("ksyun") AccessProviderTypeKubernetes = AccessProviderType("k8s") AccessProviderTypeLarkBot = AccessProviderType("larkbot") AccessProviderTypeLeCDN = AccessProviderType("lecdn") AccessProviderTypeLetsEncrypt = AccessProviderType("letsencrypt") AccessProviderTypeLetsEncryptStaging = AccessProviderType("letsencryptstaging") AccessProviderTypeLinode = AccessProviderType("linode") AccessProviderTypeLiteSSL = AccessProviderType("litessl") AccessProviderTypeLocal = AccessProviderType("local") AccessProviderTypeMattermost = AccessProviderType("mattermost") AccessProviderTypeMohua = AccessProviderType("mohua") AccessProviderTypeNamecheap = AccessProviderType("namecheap") AccessProviderTypeNameDotCom = AccessProviderType("namedotcom") AccessProviderTypeNameSilo = AccessProviderType("namesilo") AccessProviderTypeNetcup = AccessProviderType("netcup") AccessProviderTypeNetlify = AccessProviderType("netlify") AccessProviderTypeNginxProxyManager = AccessProviderType("nginxproxymanager") AccessProviderTypeNS1 = AccessProviderType("ns1") AccessProviderTypeOVHcloud = AccessProviderType("ovhcloud") AccessProviderTypePorkbun = AccessProviderType("porkbun") AccessProviderTypePowerDNS = AccessProviderType("powerdns") AccessProviderTypeProxmoxVE = AccessProviderType("proxmoxve") AccessProviderTypeQiniu = AccessProviderType("qiniu") AccessProviderTypeQingCloud = AccessProviderType("qingcloud") AccessProviderTypeRainYun = AccessProviderType("rainyun") AccessProviderTypeRatPanel = AccessProviderType("ratpanel") AccessProviderTypeRFC2136 = AccessProviderType("rfc2136") AccessProviderTypeS3 = AccessProviderType("s3") AccessProviderTypeSafeLine = AccessProviderType("safeline") AccessProviderTypeSectigo = AccessProviderType("sectigo") AccessProviderTypeSlackBot = AccessProviderType("slackbot") AccessProviderTypeSpaceship = AccessProviderType("spaceship") AccessProviderTypeSSH = AccessProviderType("ssh") AccessProviderTypeSSLCOM = AccessProviderType("sslcom") AccessProviderTypeSynologyDSM = AccessProviderType("synologydsm") AccessProviderTypeTechnitiumDNS = AccessProviderType("technitiumdns") AccessProviderTypeTelegramBot = AccessProviderType("telegrambot") AccessProviderTypeTencentCloud = AccessProviderType("tencentcloud") AccessProviderTypeTodayNIC = AccessProviderType("todaynic") AccessProviderTypeUCloud = AccessProviderType("ucloud") AccessProviderTypeUniCloud = AccessProviderType("unicloud") AccessProviderTypeUpyun = AccessProviderType("upyun") AccessProviderTypeVercel = AccessProviderType("vercel") AccessProviderTypeVolcEngine = AccessProviderType("volcengine") AccessProviderTypeVultr = AccessProviderType("vultr") AccessProviderTypeWangsu = AccessProviderType("wangsu") AccessProviderTypeWebhook = AccessProviderType("webhook") AccessProviderTypeWeComBot = AccessProviderType("wecombot") AccessProviderTypeWestcn = AccessProviderType("westcn") AccessProviderTypeXinnet = AccessProviderType("xinnet") AccessProviderTypeZeroSSL = AccessProviderType("zerossl") ) type CAProviderType string /* 证书颁发机构提供商常量值。 短横线前的部分始终等于授权提供商类型。 注意:如果追加新的常量值,请保持以 ASCII 排序。 NOTICE: If you add new constant, please keep ASCII order. */ const ( CAProviderTypeACMECA = CAProviderType(AccessProviderTypeACMECA) CAProviderTypeActalisSSL = CAProviderType(AccessProviderTypeActalisSSL) CAProviderTypeDigiCert = CAProviderType(AccessProviderTypeDigiCert) CAProviderTypeGlobalSignAtlas = CAProviderType(AccessProviderTypeGlobalSignAtlas) CAProviderTypeGoogleTrustServices = CAProviderType(AccessProviderTypeGoogleTrustServices) CAProviderTypeLetsEncrypt = CAProviderType(AccessProviderTypeLetsEncrypt) CAProviderTypeLetsEncryptStaging = CAProviderType(AccessProviderTypeLetsEncryptStaging) CAProviderTypeLiteSSL = CAProviderType(AccessProviderTypeLiteSSL) CAProviderTypeSectigo = CAProviderType(AccessProviderTypeSectigo) CAProviderTypeSSLCom = CAProviderType(AccessProviderTypeSSLCOM) CAProviderTypeZeroSSL = CAProviderType(AccessProviderTypeZeroSSL) ) type ACMEDns01ProviderType string /* ACME DNS-01 提供商常量值。 短横线前的部分始终等于授权提供商类型。 注意:如果追加新的常量值,请保持以 ASCII 排序。 NOTICE: If you add new constant, please keep ASCII order. */ const ( ACMEDns01ProviderType35cn = ACMEDns01ProviderType(AccessProviderType35cn) ACMEDns01ProviderType51DNScom = ACMEDns01ProviderType(AccessProviderType51DNScom) ACMEDns01ProviderTypeACMEDNS = ACMEDns01ProviderType(AccessProviderTypeACMEDNS) ACMEDns01ProviderTypeACMEHttpReq = ACMEDns01ProviderType(AccessProviderTypeACMEHttpReq) ACMEDns01ProviderTypeAkamai = ACMEDns01ProviderType(AccessProviderTypeAkamai) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAkamaiEdgeDNS] ACMEDns01ProviderTypeAkamaiEdgeDNS = ACMEDns01ProviderType(AccessProviderTypeAkamai + "-edgedns") ACMEDns01ProviderTypeAliyun = ACMEDns01ProviderType(AccessProviderTypeAliyun) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAliyunDNS] ACMEDns01ProviderTypeAliyunDNS = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-dns") ACMEDns01ProviderTypeAliyunESA = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-esa") ACMEDns01ProviderTypeArvanCloud = ACMEDns01ProviderType(AccessProviderTypeArvanCloud) ACMEDns01ProviderTypeAWS = ACMEDns01ProviderType(AccessProviderTypeAWS) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAWSRoute53] ACMEDns01ProviderTypeAWSRoute53 = ACMEDns01ProviderType(AccessProviderTypeAWS + "-route53") ACMEDns01ProviderTypeAzure = ACMEDns01ProviderType(AccessProviderTypeAzure) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAzure] ACMEDns01ProviderTypeAzureDNS = ACMEDns01ProviderType(AccessProviderTypeAzure + "-dns") ACMEDns01ProviderTypeBaiduCloud = ACMEDns01ProviderType(AccessProviderTypeBaiduCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeBaiduCloudDNS] ACMEDns01ProviderTypeBaiduCloudDNS = ACMEDns01ProviderType(AccessProviderTypeBaiduCloud + "-dns") ACMEDns01ProviderTypeBookMyName = ACMEDns01ProviderType(AccessProviderTypeBookMyName) ACMEDns01ProviderTypeBunny = ACMEDns01ProviderType(AccessProviderTypeBunny) ACMEDns01ProviderTypeCloudflare = ACMEDns01ProviderType(AccessProviderTypeCloudflare) ACMEDns01ProviderTypeClouDNS = ACMEDns01ProviderType(AccessProviderTypeClouDNS) ACMEDns01ProviderTypeCMCCCloud = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeCMCCCloudDNS] ACMEDns01ProviderTypeCMCCCloudDNS = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud + "-dns") ACMEDns01ProviderTypeConstellix = ACMEDns01ProviderType(AccessProviderTypeConstellix) ACMEDns01ProviderTypeCPanel = ACMEDns01ProviderType(AccessProviderTypeCPanel) ACMEDns01ProviderTypeCTCCCloud = ACMEDns01ProviderType(AccessProviderTypeCTCCCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeCTCCCloudSmartDNS] ACMEDns01ProviderTypeCTCCCloudSmartDNS = ACMEDns01ProviderType(AccessProviderTypeCTCCCloud + "-smartdns") ACMEDns01ProviderTypeDeSEC = ACMEDns01ProviderType(AccessProviderTypeDeSEC) ACMEDns01ProviderTypeDigitalOcean = ACMEDns01ProviderType(AccessProviderTypeDigitalOcean) ACMEDns01ProviderTypeDNSExit = ACMEDns01ProviderType(AccessProviderTypeDNSExit) ACMEDns01ProviderTypeDNSLA = ACMEDns01ProviderType(AccessProviderTypeDNSLA) ACMEDns01ProviderTypeDNSMadeEasy = ACMEDns01ProviderType(AccessProviderTypeDNSMadeEasy) ACMEDns01ProviderTypeDuckDNS = ACMEDns01ProviderType(AccessProviderTypeDuckDNS) ACMEDns01ProviderTypeDynu = ACMEDns01ProviderType(AccessProviderTypeDynu) ACMEDns01ProviderTypeDynv6 = ACMEDns01ProviderType(AccessProviderTypeDynv6) ACMEDns01ProviderTypeGandinet = ACMEDns01ProviderType(AccessProviderTypeGandinet) ACMEDns01ProviderTypeGcore = ACMEDns01ProviderType(AccessProviderTypeGcore) ACMEDns01ProviderTypeGname = ACMEDns01ProviderType(AccessProviderTypeGname) ACMEDns01ProviderTypeGoDaddy = ACMEDns01ProviderType(AccessProviderTypeGoDaddy) ACMEDns01ProviderTypeHetzner = ACMEDns01ProviderType(AccessProviderTypeHetzner) ACMEDns01ProviderTypeHostingde = ACMEDns01ProviderType(AccessProviderTypeHostingde) ACMEDns01ProviderTypeHostinger = ACMEDns01ProviderType(AccessProviderTypeHostinger) ACMEDns01ProviderTypeHuaweiCloud = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeHuaweiCloudDNS] ACMEDns01ProviderTypeHuaweiCloudDNS = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud + "-dns") ACMEDns01ProviderTypeInfomaniak = ACMEDns01ProviderType(AccessProviderTypeInfomaniak) ACMEDns01ProviderTypeIONOS = ACMEDns01ProviderType(AccessProviderTypeIONOS) ACMEDns01ProviderTypeJDCloud = ACMEDns01ProviderType(AccessProviderTypeJDCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeJDCloudDNS] ACMEDns01ProviderTypeJDCloudDNS = ACMEDns01ProviderType(AccessProviderTypeJDCloud + "-dns") ACMEDns01ProviderTypeLinode = ACMEDns01ProviderType(AccessProviderTypeLinode) ACMEDns01ProviderTypeNamecheap = ACMEDns01ProviderType(AccessProviderTypeNamecheap) ACMEDns01ProviderTypeNameDotCom = ACMEDns01ProviderType(AccessProviderTypeNameDotCom) ACMEDns01ProviderTypeNameSilo = ACMEDns01ProviderType(AccessProviderTypeNameSilo) ACMEDns01ProviderTypeNetcup = ACMEDns01ProviderType(AccessProviderTypeNetcup) ACMEDns01ProviderTypeNetlify = ACMEDns01ProviderType(AccessProviderTypeNetlify) ACMEDns01ProviderTypeNS1 = ACMEDns01ProviderType(AccessProviderTypeNS1) ACMEDns01ProviderTypeOVHcloud = ACMEDns01ProviderType(AccessProviderTypeOVHcloud) ACMEDns01ProviderTypePorkbun = ACMEDns01ProviderType(AccessProviderTypePorkbun) ACMEDns01ProviderTypePowerDNS = ACMEDns01ProviderType(AccessProviderTypePowerDNS) ACMEDns01ProviderTypeQingCloud = ACMEDns01ProviderType(AccessProviderTypeQingCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeQingCloudDNS] ACMEDns01ProviderTypeQingCloudDNS = ACMEDns01ProviderType(AccessProviderTypeQingCloud + "-dns") ACMEDns01ProviderTypeRainYun = ACMEDns01ProviderType(AccessProviderTypeRainYun) ACMEDns01ProviderTypeRFC2136 = ACMEDns01ProviderType(AccessProviderTypeRFC2136) ACMEDns01ProviderTypeSpaceship = ACMEDns01ProviderType(AccessProviderTypeSpaceship) ACMEDns01ProviderTypeTechnitiumDNS = ACMEDns01ProviderType(AccessProviderTypeTechnitiumDNS) ACMEDns01ProviderTypeTencentCloud = ACMEDns01ProviderType(AccessProviderTypeTencentCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeTencentCloudDNS] ACMEDns01ProviderTypeTencentCloudDNS = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-dns") ACMEDns01ProviderTypeTencentCloudEO = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-eo") ACMEDns01ProviderTypeTodayNIC = ACMEDns01ProviderType(AccessProviderTypeTodayNIC) ACMEDns01ProviderTypeUCloud = ACMEDns01ProviderType(AccessProviderTypeUCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeUCloudUDNR] ACMEDns01ProviderTypeUCloudUDNR = ACMEDns01ProviderType(AccessProviderTypeUCloud + "-udnr") ACMEDns01ProviderTypeVercel = ACMEDns01ProviderType(AccessProviderTypeVercel) ACMEDns01ProviderTypeVolcEngine = ACMEDns01ProviderType(AccessProviderTypeVolcEngine) // 兼容旧值,等同于 [ACMEDns01ProviderTypeVolcEngineDNS] ACMEDns01ProviderTypeVolcEngineDNS = ACMEDns01ProviderType(AccessProviderTypeVolcEngine + "-dns") ACMEDns01ProviderTypeVultr = ACMEDns01ProviderType(AccessProviderTypeVultr) ACMEDns01ProviderTypeWestcn = ACMEDns01ProviderType(AccessProviderTypeWestcn) ACMEDns01ProviderTypeXinnet = ACMEDns01ProviderType(AccessProviderTypeXinnet) ) type ACMEHttp01ProviderType string /* ACME HTTP-01 提供商常量值。 短横线前的部分始终等于授权提供商类型。 注意:如果追加新的常量值,请保持以 ASCII 排序。 NOTICE: If you add new constant, please keep ASCII order. */ const ( ACMEHttp01ProviderTypeLocal = ACMEHttp01ProviderType(AccessProviderTypeLocal) ACMEHttp01ProviderTypeS3 = ACMEHttp01ProviderType(AccessProviderTypeS3) ACMEHttp01ProviderTypeSSH = ACMEHttp01ProviderType(AccessProviderTypeSSH) ) type DeploymentProviderType string /* 部署证书主机提供商常量值。 短横线前的部分始终等于授权提供商类型。 注意:如果追加新的常量值,请保持以 ASCII 排序。 NOTICE: If you add new constant, please keep ASCII order. */ const ( DeploymentProviderType1Panel = DeploymentProviderType(AccessProviderType1Panel) DeploymentProviderType1PanelConsole = DeploymentProviderType(AccessProviderType1Panel + "-console") DeploymentProviderTypeAliyunALB = DeploymentProviderType(AccessProviderTypeAliyun + "-alb") DeploymentProviderTypeAliyunAPIGW = DeploymentProviderType(AccessProviderTypeAliyun + "-apigw") DeploymentProviderTypeAliyunCAS = DeploymentProviderType(AccessProviderTypeAliyun + "-cas") DeploymentProviderTypeAliyunCASDeploy = DeploymentProviderType(AccessProviderTypeAliyun + "-casdeploy") DeploymentProviderTypeAliyunCDN = DeploymentProviderType(AccessProviderTypeAliyun + "-cdn") DeploymentProviderTypeAliyunCLB = DeploymentProviderType(AccessProviderTypeAliyun + "-clb") DeploymentProviderTypeAliyunDCDN = DeploymentProviderType(AccessProviderTypeAliyun + "-dcdn") DeploymentProviderTypeAliyunDDoSPro = DeploymentProviderType(AccessProviderTypeAliyun + "-ddospro") DeploymentProviderTypeAliyunESA = DeploymentProviderType(AccessProviderTypeAliyun + "-esa") DeploymentProviderTypeAliyunESASaaS = DeploymentProviderType(AccessProviderTypeAliyun + "-esasaas") DeploymentProviderTypeAliyunFC = DeploymentProviderType(AccessProviderTypeAliyun + "-fc") DeploymentProviderTypeAliyunGA = DeploymentProviderType(AccessProviderTypeAliyun + "-ga") DeploymentProviderTypeAliyunLive = DeploymentProviderType(AccessProviderTypeAliyun + "-live") DeploymentProviderTypeAliyunNLB = DeploymentProviderType(AccessProviderTypeAliyun + "-nlb") DeploymentProviderTypeAliyunOSS = DeploymentProviderType(AccessProviderTypeAliyun + "-oss") DeploymentProviderTypeAliyunVOD = DeploymentProviderType(AccessProviderTypeAliyun + "-vod") DeploymentProviderTypeAliyunWAF = DeploymentProviderType(AccessProviderTypeAliyun + "-waf") DeploymentProviderTypeAPISIX = DeploymentProviderType(AccessProviderTypeAPISIX) DeploymentProviderTypeAWSACM = DeploymentProviderType(AccessProviderTypeAWS + "-acm") DeploymentProviderTypeAWSCloudFront = DeploymentProviderType(AccessProviderTypeAWS + "-cloudfront") DeploymentProviderTypeAWSIAM = DeploymentProviderType(AccessProviderTypeAWS + "-iam") DeploymentProviderTypeAzureKeyVault = DeploymentProviderType(AccessProviderTypeAzure + "-keyvault") DeploymentProviderTypeBaiduCloudAppBLB = DeploymentProviderType(AccessProviderTypeBaiduCloud + "-appblb") DeploymentProviderTypeBaiduCloudBLB = DeploymentProviderType(AccessProviderTypeBaiduCloud + "-blb") DeploymentProviderTypeBaiduCloudCDN = DeploymentProviderType(AccessProviderTypeBaiduCloud + "-cdn") DeploymentProviderTypeBaiduCloudCert = DeploymentProviderType(AccessProviderTypeBaiduCloud + "-cert") DeploymentProviderTypeBaishanCDN = DeploymentProviderType(AccessProviderTypeBaishan + "-cdn") DeploymentProviderTypeBaotaPanel = DeploymentProviderType(AccessProviderTypeBaotaPanel) DeploymentProviderTypeBaotaPanelConsole = DeploymentProviderType(AccessProviderTypeBaotaPanel + "-console") DeploymentProviderTypeBaotaPanelGo = DeploymentProviderType(AccessProviderTypeBaotaPanelGo) DeploymentProviderTypeBaotaPanelGoConsole = DeploymentProviderType(AccessProviderTypeBaotaPanelGo + "-console") DeploymentProviderTypeBaotaWAF = DeploymentProviderType(AccessProviderTypeBaotaWAF) DeploymentProviderTypeBaotaWAFConsole = DeploymentProviderType(AccessProviderTypeBaotaWAF + "-console") DeploymentProviderTypeBunnyCDN = DeploymentProviderType(AccessProviderTypeBunny + "-cdn") DeploymentProviderTypeBytePlusCDN = DeploymentProviderType(AccessProviderTypeBytePlus + "-cdn") DeploymentProviderTypeCacheFly = DeploymentProviderType(AccessProviderTypeCacheFly) DeploymentProviderTypeCdnfly = DeploymentProviderType(AccessProviderTypeCdnfly) DeploymentProviderTypeCPanel = DeploymentProviderType(AccessProviderTypeCPanel) DeploymentProviderTypeCTCCCloudAO = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-ao") DeploymentProviderTypeCTCCCloudCDN = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-cdn") DeploymentProviderTypeCTCCCloudCMS = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-cms") DeploymentProviderTypeCTCCCloudELB = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-elb") DeploymentProviderTypeCTCCCloudFaaS = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-faas") DeploymentProviderTypeCTCCCloudICDN = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-icdn") DeploymentProviderTypeCTCCCloudLVDN = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-ldvn") DeploymentProviderTypeDogeCloudCDN = DeploymentProviderType(AccessProviderTypeDogeCloud + "-cdn") DeploymentProviderTypeDokploy = DeploymentProviderType(AccessProviderTypeDokploy) DeploymentProviderTypeFlexCDN = DeploymentProviderType(AccessProviderTypeFlexCDN) DeploymentProviderTypeFlyIO = DeploymentProviderType(AccessProviderTypeFlyIO) DeploymentProviderTypeGcoreCDN = DeploymentProviderType(AccessProviderTypeGcore + "-cdn") DeploymentProviderTypeGoEdge = DeploymentProviderType(AccessProviderTypeGoEdge) DeploymentProviderTypeHuaweiCloudCDN = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-cdn") DeploymentProviderTypeHuaweiCloudELB = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-elb") DeploymentProviderTypeHuaweiCloudSCM = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-scm") DeploymentProviderTypeHuaweiCloudOBS = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-obs") DeploymentProviderTypeHuaweiCloudWAF = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-waf") DeploymentProviderTypeJDCloudALB = DeploymentProviderType(AccessProviderTypeJDCloud + "-alb") DeploymentProviderTypeJDCloudCDN = DeploymentProviderType(AccessProviderTypeJDCloud + "-cdn") DeploymentProviderTypeJDCloudLive = DeploymentProviderType(AccessProviderTypeJDCloud + "-live") DeploymentProviderTypeJDCloudVOD = DeploymentProviderType(AccessProviderTypeJDCloud + "-vod") DeploymentProviderTypeKong = DeploymentProviderType(AccessProviderTypeKong) DeploymentProviderTypeKubernetesSecret = DeploymentProviderType(AccessProviderTypeKubernetes + "-secret") DeploymentProviderTypeKsyunCDN = DeploymentProviderType(AccessProviderTypeKsyun + "-cdn") DeploymentProviderTypeLeCDN = DeploymentProviderType(AccessProviderTypeLeCDN) DeploymentProviderTypeLocal = DeploymentProviderType(AccessProviderTypeLocal) DeploymentProviderTypeMohuaMVH = DeploymentProviderType(AccessProviderTypeMohua + "-mvh") DeploymentProviderTypeNetlify = DeploymentProviderType(AccessProviderTypeNetlify) DeploymentProviderTypeNginxProxyManager = DeploymentProviderType(AccessProviderTypeNginxProxyManager) DeploymentProviderTypeProxmoxVE = DeploymentProviderType(AccessProviderTypeProxmoxVE) DeploymentProviderTypeQiniuCDN = DeploymentProviderType(AccessProviderTypeQiniu + "-cdn") DeploymentProviderTypeQiniuKodo = DeploymentProviderType(AccessProviderTypeQiniu + "-kodo") DeploymentProviderTypeQiniuPili = DeploymentProviderType(AccessProviderTypeQiniu + "-pili") DeploymentProviderTypeRainYunRCDN = DeploymentProviderType(AccessProviderTypeRainYun + "-rcdn") DeploymentProviderTypeRainYunSSLCenter = DeploymentProviderType(AccessProviderTypeRainYun + "-sslcenter") DeploymentProviderTypeRatPanel = DeploymentProviderType(AccessProviderTypeRatPanel) DeploymentProviderTypeRatPanelConsole = DeploymentProviderType(AccessProviderTypeRatPanel + "-console") DeploymentProviderTypeS3 = DeploymentProviderType(AccessProviderTypeS3) DeploymentProviderTypeSafeLine = DeploymentProviderType(AccessProviderTypeSafeLine) DeploymentProviderTypeSSH = DeploymentProviderType(AccessProviderTypeSSH) DeploymentProviderTypeSynologyDSM = DeploymentProviderType(AccessProviderTypeSynologyDSM) DeploymentProviderTypeTencentCloudCDN = DeploymentProviderType(AccessProviderTypeTencentCloud + "-cdn") DeploymentProviderTypeTencentCloudCLB = DeploymentProviderType(AccessProviderTypeTencentCloud + "-clb") DeploymentProviderTypeTencentCloudCOS = DeploymentProviderType(AccessProviderTypeTencentCloud + "-cos") DeploymentProviderTypeTencentCloudCSS = DeploymentProviderType(AccessProviderTypeTencentCloud + "-css") DeploymentProviderTypeTencentCloudECDN = DeploymentProviderType(AccessProviderTypeTencentCloud + "-ecdn") DeploymentProviderTypeTencentCloudEO = DeploymentProviderType(AccessProviderTypeTencentCloud + "-eo") DeploymentProviderTypeTencentCloudGAAP = DeploymentProviderType(AccessProviderTypeTencentCloud + "-gaap") DeploymentProviderTypeTencentCloudSCF = DeploymentProviderType(AccessProviderTypeTencentCloud + "-scf") DeploymentProviderTypeTencentCloudSSL = DeploymentProviderType(AccessProviderTypeTencentCloud + "-ssl") DeploymentProviderTypeTencentCloudSSLDeploy = DeploymentProviderType(AccessProviderTypeTencentCloud + "-ssldeploy") DeploymentProviderTypeTencentCloudSSLUpdate = DeploymentProviderType(AccessProviderTypeTencentCloud + "-sslupdate") DeploymentProviderTypeTencentCloudVOD = DeploymentProviderType(AccessProviderTypeTencentCloud + "-vod") DeploymentProviderTypeTencentCloudWAF = DeploymentProviderType(AccessProviderTypeTencentCloud + "-waf") DeploymentProviderTypeUCloudUALB = DeploymentProviderType(AccessProviderTypeUCloud + "-ualb") DeploymentProviderTypeUCloudUCDN = DeploymentProviderType(AccessProviderTypeUCloud + "-ucdn") DeploymentProviderTypeUCloudUCLB = DeploymentProviderType(AccessProviderTypeUCloud + "-uclb") DeploymentProviderTypeUCloudUEWAF = DeploymentProviderType(AccessProviderTypeUCloud + "-uewaf") DeploymentProviderTypeUCloudUPathX = DeploymentProviderType(AccessProviderTypeUCloud + "-pathx") DeploymentProviderTypeUCloudUS3 = DeploymentProviderType(AccessProviderTypeUCloud + "-us3") DeploymentProviderTypeUniCloudWebHost = DeploymentProviderType(AccessProviderTypeUniCloud + "-webhost") DeploymentProviderTypeUpyunCDN = DeploymentProviderType(AccessProviderTypeUpyun + "-cdn") DeploymentProviderTypeUpyunFile = DeploymentProviderType(AccessProviderTypeUpyun + "-file") DeploymentProviderTypeVolcEngineALB = DeploymentProviderType(AccessProviderTypeVolcEngine + "-alb") DeploymentProviderTypeVolcEngineCDN = DeploymentProviderType(AccessProviderTypeVolcEngine + "-cdn") DeploymentProviderTypeVolcEngineCertCenter = DeploymentProviderType(AccessProviderTypeVolcEngine + "-certcenter") DeploymentProviderTypeVolcEngineCLB = DeploymentProviderType(AccessProviderTypeVolcEngine + "-clb") DeploymentProviderTypeVolcEngineDCDN = DeploymentProviderType(AccessProviderTypeVolcEngine + "-dcdn") DeploymentProviderTypeVolcEngineImageX = DeploymentProviderType(AccessProviderTypeVolcEngine + "-imagex") DeploymentProviderTypeVolcEngineLive = DeploymentProviderType(AccessProviderTypeVolcEngine + "-live") DeploymentProviderTypeVolcEngineTOS = DeploymentProviderType(AccessProviderTypeVolcEngine + "-tos") DeploymentProviderTypeVolcEngineVOD = DeploymentProviderType(AccessProviderTypeVolcEngine + "-vod") DeploymentProviderTypeVolcEngineWAF = DeploymentProviderType(AccessProviderTypeVolcEngine + "-waf") DeploymentProviderTypeWangsuCDN = DeploymentProviderType(AccessProviderTypeWangsu + "-cdn") DeploymentProviderTypeWangsuCDNPro = DeploymentProviderType(AccessProviderTypeWangsu + "-cdnpro") DeploymentProviderTypeWangsuCertificate = DeploymentProviderType(AccessProviderTypeWangsu + "-certificate") DeploymentProviderTypeWebhook = DeploymentProviderType(AccessProviderTypeWebhook) ) type NotificationProviderType string /* 消息通知提供商常量值。 短横线前的部分始终等于授权提供商类型。 注意:如果追加新的常量值,请保持以 ASCII 排序。 NOTICE: If you add new constant, please keep ASCII order. */ const ( NotificationProviderTypeDingTalkBot = NotificationProviderType(AccessProviderTypeDingTalkBot) NotificationProviderTypeDiscordBot = NotificationProviderType(AccessProviderTypeDiscordBot) NotificationProviderTypeEmail = NotificationProviderType(AccessProviderTypeEmail) NotificationProviderTypeLarkBot = NotificationProviderType(AccessProviderTypeLarkBot) NotificationProviderTypeMattermost = NotificationProviderType(AccessProviderTypeMattermost) NotificationProviderTypeSlackBot = NotificationProviderType(AccessProviderTypeSlackBot) NotificationProviderTypeTelegramBot = NotificationProviderType(AccessProviderTypeTelegramBot) NotificationProviderTypeWebhook = NotificationProviderType(AccessProviderTypeWebhook) NotificationProviderTypeWeComBot = NotificationProviderType(AccessProviderTypeWeComBot) ) ================================================ FILE: internal/domain/settings.go ================================================ package domain import ( xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) const CollectionNameSettings = "settings" type Settings struct { Meta Name string `db:"name" json:"name"` Content SettingsContent `db:"content" json:"content"` } const ( SettingsNameEmails = "emails" SettingsNameNotificationTemplate = "notifyTemplate" SettingsNameScriptTemplate = "scriptTemplate" SettingsNameSSLProvider = "sslProvider" SettingsNamePersistence = "persistence" ) type SettingsContent map[string]any type SettingsContentForSSLProvider struct { Provider CAProviderType `json:"provider"` Configs map[CAProviderType]map[string]any `json:"configs"` Timeout int `json:"timeout"` } type SettingsContentForPersistence struct { CertificatesWarningDaysBeforeExpire int `json:"certificatesWarningDaysBeforeExpire"` CertificatesRetentionMaxDays int `json:"certificatesRetentionMaxDays"` WorkflowRunsRetentionMaxDays int `json:"workflowRunsRetentionMaxDays"` } func (c SettingsContent) AsSSLProvider() *SettingsContentForSSLProvider { content := &SettingsContentForSSLProvider{} xmaps.Populate(c, content) if content.Provider == "" { content.Provider = CAProviderTypeLetsEncrypt } if content.Timeout < 0 { content.Timeout = 0 } return content } func (c SettingsContent) AsPersistence() *SettingsContentForPersistence { content := &SettingsContentForPersistence{} xmaps.Populate(c, content) if content.CertificatesWarningDaysBeforeExpire <= 0 { content.CertificatesWarningDaysBeforeExpire = 21 } if content.CertificatesRetentionMaxDays < 0 { content.CertificatesRetentionMaxDays = 0 } if content.WorkflowRunsRetentionMaxDays < 0 { content.WorkflowRunsRetentionMaxDays = 0 } return content } ================================================ FILE: internal/domain/statistics.go ================================================ package domain type Statistics struct { CertificateTotal int `json:"certificateTotal"` CertificateExpiringSoon int `json:"certificateExpiringSoon"` CertificateExpired int `json:"certificateExpired"` WorkflowTotal int `json:"workflowTotal"` WorkflowEnabled int `json:"workflowEnabled"` WorkflowDisabled int `json:"workflowDisabled"` } ================================================ FILE: internal/domain/workflow.go ================================================ package domain import ( "encoding/json" "fmt" "strings" "time" "github.com/samber/lo" "github.com/certimate-go/certimate/internal/domain/expr" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) const CollectionNameWorkflow = "workflow" type Workflow struct { Meta Name string `db:"name" json:"name"` Description string `db:"description" json:"description"` Trigger WorkflowTriggerType `db:"trigger" json:"trigger"` TriggerCron string `db:"triggerCron" json:"triggerCron"` Enabled bool `db:"enabled" json:"enabled"` GraphDraft *WorkflowGraph `db:"graphDraft" json:"graphDraft"` GraphContent *WorkflowGraph `db:"graphContent" json:"graphContent"` HasDraft bool `db:"hasDraft" json:"hasDraft"` HasContent bool `db:"hasContent" json:"hasContent"` LastRunId string `db:"lastRunRef" json:"lastRunId"` LastRunStatus WorkflowRunStatusType `db:"lastRunStatus" json:"lastRunStatus"` LastRunTime time.Time `db:"lastRunTime" json:"lastRunTime"` } type WorkflowGraph struct { Nodes []*WorkflowNode `json:"nodes"` } func (g *WorkflowGraph) GetNodeById(nodeId string) (*WorkflowNode, bool) { return g.getNodeInBlocksById(g.Nodes, nodeId) } func (g *WorkflowGraph) getNodeInBlocksById(blocks []*WorkflowNode, nodeId string) (*WorkflowNode, bool) { for _, node := range blocks { if node.Id == nodeId { return node, true } if len(node.Blocks) > 0 { if found, ok := g.getNodeInBlocksById(node.Blocks, nodeId); ok { return found, true } } } return nil, false } func (g *WorkflowGraph) Verify() error { if len(g.Nodes) < 2 { return fmt.Errorf("invalid nodes length of graph") } else if g.Nodes[0].Type != WorkflowNodeTypeStart { return fmt.Errorf("the first node is not a start node") } else if g.Nodes[len(g.Nodes)-1].Type != WorkflowNodeTypeEnd { return fmt.Errorf("the last node is not an end node") } return nil } func (g *WorkflowGraph) Clone() *WorkflowGraph { return &WorkflowGraph{ Nodes: g.Nodes, } } type WorkflowTriggerType string const ( WorkflowTriggerTypeScheduled = WorkflowTriggerType("scheduled") WorkflowTriggerTypeManual = WorkflowTriggerType("manual") ) type WorkflowNode struct { Id string `json:"id"` // 节点 ID 只在该工作流中唯一,在全局中不保证唯一性 Type WorkflowNodeType `json:"type"` Data WorkflowNodeData `json:"data"` Blocks []*WorkflowNode `json:"blocks,omitempty"` } type WorkflowNodeType string const ( WorkflowNodeTypeStart = WorkflowNodeType("start") WorkflowNodeTypeEnd = WorkflowNodeType("end") WorkflowNodeTypeCondition = WorkflowNodeType("condition") WorkflowNodeTypeBranchBlock = WorkflowNodeType("branchBlock") WorkflowNodeTypeTryCatch = WorkflowNodeType("tryCatch") WorkflowNodeTypeTryBlock = WorkflowNodeType("tryBlock") WorkflowNodeTypeCatchBlock = WorkflowNodeType("catchBlock") WorkflowNodeTypeDelay = WorkflowNodeType("delay") WorkflowNodeTypeBizApply = WorkflowNodeType("bizApply") WorkflowNodeTypeBizUpload = WorkflowNodeType("bizUpload") WorkflowNodeTypeBizMonitor = WorkflowNodeType("bizMonitor") WorkflowNodeTypeBizDeploy = WorkflowNodeType("bizDeploy") WorkflowNodeTypeBizNotify = WorkflowNodeType("bizNotify") ) type WorkflowNodeData struct { Name string `json:"name"` Disabled bool `json:"disabled,omitempty,omitzero"` Config WorkflowNodeConfig `json:"config,omitempty,omitzero"` } type WorkflowNodeConfig map[string]any func (c WorkflowNodeConfig) AsDelay() WorkflowNodeConfigForDelay { return WorkflowNodeConfigForDelay{ Wait: xmaps.GetInt(c, "wait"), } } func (c WorkflowNodeConfig) AsBranchBlock() WorkflowNodeConfigForBranchBlock { expression := c["expression"] if expression == nil { return WorkflowNodeConfigForBranchBlock{} } exprRaw, _ := json.Marshal(expression) expr, err := expr.UnmarshalExpr([]byte(exprRaw)) if err != nil { return WorkflowNodeConfigForBranchBlock{} } return WorkflowNodeConfigForBranchBlock{ Expression: expr, } } func (c WorkflowNodeConfig) AsBizApply() WorkflowNodeConfigForBizApply { domains := lo.Filter(strings.Split(xmaps.GetString(c, "domains"), ";"), func(s string, _ int) bool { return s != "" }) ipaddrs := lo.Filter(strings.Split(xmaps.GetString(c, "ipaddrs"), ";"), func(s string, _ int) bool { return s != "" }) nameservers := lo.Filter(strings.Split(xmaps.GetString(c, "nameservers"), ";"), func(s string, _ int) bool { return s != "" }) return WorkflowNodeConfigForBizApply{ Domains: domains, IPAddrs: ipaddrs, ContactEmail: xmaps.GetString(c, "contactEmail"), ChallengeType: xmaps.GetString(c, "challengeType"), Provider: xmaps.GetString(c, "provider"), ProviderAccessId: xmaps.GetString(c, "providerAccessId"), ProviderConfig: xmaps.GetKVMapAny(c, "providerConfig"), KeySource: xmaps.GetOrDefaultString(c, "keySource", "auto"), KeyAlgorithm: xmaps.GetOrDefaultString(c, "keyAlgorithm", string(CertificateKeyAlgorithmTypeRSA2048)), KeyContent: xmaps.GetString(c, "keyContent"), CAProvider: xmaps.GetString(c, "caProvider"), CAProviderAccessId: xmaps.GetString(c, "caProviderAccessId"), CAProviderConfig: xmaps.GetKVMapAny(c, "caProviderConfig"), ValidityLifetime: xmaps.GetString(c, "validityLifetime"), PreferredChain: xmaps.GetString(c, "preferredChain"), ACMEProfile: xmaps.GetString(c, "acmeProfile"), Nameservers: nameservers, DnsPropagationWait: xmaps.GetInt(c, "dnsPropagationWait"), DnsPropagationTimeout: xmaps.GetInt(c, "dnsPropagationTimeout"), DnsTTL: xmaps.GetInt(c, "dnsTTL"), HttpDelayWait: xmaps.GetInt(c, "httpDelayWait"), DisableCommonName: xmaps.GetBool(c, "disableCommonName"), DisableFollowCNAME: xmaps.GetBool(c, "disableFollowCNAME"), DisableARI: xmaps.GetBool(c, "disableARI"), SkipBeforeExpiryDays: xmaps.GetInt(c, "skipBeforeExpiryDays"), } } func (c WorkflowNodeConfig) AsBizUpload() WorkflowNodeConfigForBizUpload { return WorkflowNodeConfigForBizUpload{ Source: xmaps.GetOrDefaultString(c, "source", "form"), Certificate: xmaps.GetString(c, "certificate"), PrivateKey: xmaps.GetString(c, "privateKey"), } } func (c WorkflowNodeConfig) AsBizMonitor() WorkflowNodeConfigForBizMonitor { host := xmaps.GetString(c, "host") return WorkflowNodeConfigForBizMonitor{ Host: host, Port: xmaps.GetOrDefaultInt32(c, "port", 443), Domain: xmaps.GetOrDefaultString(c, "domain", host), RequestPath: xmaps.GetString(c, "path"), } } func (c WorkflowNodeConfig) AsBizDeploy() WorkflowNodeConfigForBizDeploy { return WorkflowNodeConfigForBizDeploy{ CertificateOutputNodeId: xmaps.GetString(c, "certificateOutputNodeId"), Provider: xmaps.GetString(c, "provider"), ProviderAccessId: xmaps.GetString(c, "providerAccessId"), ProviderConfig: xmaps.GetKVMapAny(c, "providerConfig"), SkipOnLastSucceeded: xmaps.GetBool(c, "skipOnLastSucceeded"), } } func (c WorkflowNodeConfig) AsBizNotify() WorkflowNodeConfigForBizNotify { return WorkflowNodeConfigForBizNotify{ Provider: xmaps.GetString(c, "provider"), ProviderAccessId: xmaps.GetString(c, "providerAccessId"), ProviderConfig: xmaps.GetKVMapAny(c, "providerConfig"), Subject: xmaps.GetString(c, "subject"), Message: xmaps.GetString(c, "message"), SkipOnAllPrevSkipped: xmaps.GetBool(c, "skipOnAllPrevSkipped"), } } type WorkflowNodeConfigForDelay struct { Wait int `json:"wait"` // 等待时间 } type WorkflowNodeConfigForBranchBlock struct { Expression expr.Expr `json:"expression"` // 条件表达式 } type WorkflowNodeConfigForBizApply struct { Domains []string `json:"domains"` // 域名列表,以半角分号分隔 IPAddrs []string `json:"ipaddrs"` // IP 地址列表,以半角分号分隔 ContactEmail string `json:"contactEmail"` // 联系邮箱 ChallengeType string `json:"challengeType"` // 质询方式 Provider string `json:"provider"` // 质询提供商 ProviderAccessId string `json:"providerAccessId"` // 质询提供商授权记录 ID ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 质询提供商额外配置 CAProvider string `json:"caProvider,omitempty"` // CA 提供商(零值时使用全局配置) CAProviderAccessId string `json:"caProviderAccessId,omitempty"` // CA 提供商授权记录 ID CAProviderConfig map[string]any `json:"caProviderConfig,omitempty"` // CA 提供商额外配置 KeySource string `json:"keySource"` // 私钥来源,可取值 "auto"、"reuse"、"custom"(零值时默认值 "auto") KeyAlgorithm string `json:"keyAlgorithm,omitempty"` // 私钥算法 KeyContent string `json:"keyContent,omitempty"` // 私钥内容 ValidityLifetime string `json:"validityLifetime,omitempty"` // 有效期,形如 "30d"、"6h" PreferredChain string `json:"preferredChain,omitempty"` // 首选证书链 ACMEProfile string `json:"acmeProfile,omitempty"` // ACME Profiles Extension Nameservers []string `json:"nameservers,omitempty"` // DNS 服务器列表,以半角分号分隔。等同于 lego 的 `--dns.resolvers` 参数 DnsPropagationWait int `json:"dnsPropagationWait,omitempty"` // DNS 传播等待时间。等同于 lego 的 `--dns.propagation-wait` 参数 DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` // DNS 传播检查超时时间。等同于 lego 的 `--dns-timeout` 参数 DnsTTL int `json:"dnsTTL,omitempty"` // DNS 解析记录 TTL HttpDelayWait int `json:"httpDelayWait,omitempty"` // HTTP 等待时间。等同于 lego 的 `--http.delay` 参数 DisableCommonName bool `json:"disableCommonName,omitempty"` // 是否不包含 CommonName。等同于 lego 的 `--disable-cn` 参数 DisableFollowCNAME bool `json:"disableFollowCNAME,omitempty"` // 是否关闭 CNAME 跟随 DisableARI bool `json:"disableARI,omitempty"` // 是否关闭 ARI SkipBeforeExpiryDays int `json:"skipBeforeExpiryDays,omitempty"` // 证书到期前多少天前跳过续期 } type WorkflowNodeConfigForBizUpload struct { Source string `json:"source"` // 证书来源,可取值 "form"、"local"、"url"(零值时默认值 "form") Certificate string `json:"certificate"` // 证书,根据证书来源决定是 PEM 内容 / 文件路径 / URL PrivateKey string `json:"privateKey"` // 私钥,根据证书来源决定是 PEM 内容 / 文件路径 / URL } type WorkflowNodeConfigForBizMonitor struct { Host string `json:"host"` // 主机地址 Port int32 `json:"port,omitempty"` // 端口(零值时默认值 443) Domain string `json:"domain,omitempty"` // 域名(零值时默认值 [Host]) RequestPath string `json:"requestPath,omitempty"` // 请求路径 } type WorkflowNodeConfigForBizDeploy struct { CertificateOutputNodeId string `json:"certificateOutputNodeId"` // 前序证书输出节点 ID Provider string `json:"provider"` // 主机提供商 ProviderAccessId string `json:"providerAccessId,omitempty"` // 主机提供商授权记录 ID ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 主机提供商额外配置 SkipOnLastSucceeded bool `json:"skipOnLastSucceeded"` // 上次部署成功时是否跳过 } type WorkflowNodeConfigForBizNotify struct { Provider string `json:"provider"` // 通知提供商 ProviderAccessId string `json:"providerAccessId"` // 通知提供商授权记录 ID ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 通知提供商额外配置 Subject string `json:"subject"` // 通知主题 Message string `json:"message"` // 通知内容 SkipOnAllPrevSkipped bool `json:"skipOnAllPrevSkipped"` // 前序节点均已跳过时是否跳过 } ================================================ FILE: internal/domain/workflow_log.go ================================================ package domain import ( "log/slog" "strings" ) const CollectionNameWorkflowLog = "workflow_logs" type WorkflowLog struct { Meta WorkflowId string `db:"workflowRef" json:"workflowId"` RunId string `db:"runRef" json:"runId"` NodeId string `db:"nodeId" json:"nodeId"` NodeName string `db:"nodeName" json:"nodeName"` TimestampMilli int64 `db:"timestamp" json:"timestamp"` Level int32 `db:"level" json:"level"` Message string `db:"message" json:"message"` Data map[string]any `db:"data" json:"data"` } type WorkflowLogs []WorkflowLog func (r WorkflowLogs) ErrorString() string { var builder strings.Builder for _, log := range r { if log.Level >= int32(slog.LevelError) { builder.WriteString(log.Message) builder.WriteString("\n") } } return strings.TrimSpace(builder.String()) } ================================================ FILE: internal/domain/workflow_output.go ================================================ package domain const CollectionNameWorkflowOutput = "workflow_output" type WorkflowOutput struct { Meta WorkflowId string `db:"workflowRef" json:"workflowId"` RunId string `db:"runRef" json:"runId"` NodeId string `db:"nodeId" json:"nodeId"` NodeConfig WorkflowNodeConfig `db:"nodeConfig" json:"nodeConfig"` Outputs []*WorkflowOutputEntry `db:"outputs" json:"outputs"` Succeeded bool `db:"succeeded" json:"succeeded"` } type WorkflowOutputEntry struct { Type string `json:"type"` Name string `json:"name"` Value string `json:"value"` ValueType string `json:"valueType"` } ================================================ FILE: internal/domain/workflow_run.go ================================================ package domain import ( "time" ) const CollectionNameWorkflowRun = "workflow_run" type WorkflowRun struct { Meta WorkflowId string `db:"workflowRef" json:"workflowId"` Status WorkflowRunStatusType `db:"status" json:"status"` Trigger WorkflowTriggerType `db:"trigger" json:"trigger"` StartedAt time.Time `db:"startedAt" json:"startedAt"` EndedAt time.Time `db:"endedAt" json:"endedAt"` Graph *WorkflowGraph `db:"graph" json:"graph"` Error string `db:"error" json:"error"` } type WorkflowRunStatusType string const ( WorkflowRunStatusTypePending WorkflowRunStatusType = "pending" WorkflowRunStatusTypeProcessing WorkflowRunStatusType = "processing" WorkflowRunStatusTypeSucceeded WorkflowRunStatusType = "succeeded" WorkflowRunStatusTypeFailed WorkflowRunStatusType = "failed" WorkflowRunStatusTypeCanceled WorkflowRunStatusType = "canceled" ) ================================================ FILE: internal/notify/client.go ================================================ package notify import ( "log/slog" ) type Client struct { logger *slog.Logger } type ClientConfigure func(*Client) func NewClient(configures ...ClientConfigure) *Client { client := &Client{} for _, configure := range configures { configure(client) } return client } func WithLogger(logger *slog.Logger) ClientConfigure { return func(c *Client) { c.logger = logger } } ================================================ FILE: internal/notify/client_notifier.go ================================================ package notify import ( "context" "errors" "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/internal/notify/notifiers" ) type SendNotificationRequest struct { // 提供商相关 Provider string ProviderAccessConfig map[string]any ProviderExtendedConfig map[string]any // 通知相关 Subject string Message string } type SendNotificationResponse struct{} func (c *Client) SendNotification(ctx context.Context, request *SendNotificationRequest) (*SendNotificationResponse, error) { if request == nil { return nil, errors.New("the request is nil") } providerFactory, err := notifiers.Registries.Get(domain.NotificationProviderType(request.Provider)) if err != nil { return nil, err } provider, err := providerFactory(¬ifiers.ProviderFactoryOptions{ ProviderAccessConfig: request.ProviderAccessConfig, ProviderExtendedConfig: request.ProviderExtendedConfig, }) if err != nil { return nil, fmt.Errorf("failed to initialize notification provider '%s': %w", request.Provider, err) } provider.SetLogger(c.logger) if _, err := provider.Notify(ctx, request.Subject, request.Message); err != nil { return nil, err } return &SendNotificationResponse{}, nil } ================================================ FILE: internal/notify/notifiers/registry.go ================================================ package notifiers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/notifier" ) type ProviderFactoryFunc func(options *ProviderFactoryOptions) (notifier.Provider, error) type ProviderFactoryOptions struct { ProviderAccessConfig map[string]any ProviderExtendedConfig map[string]any } type Registry[T comparable] interface { Register(T, ProviderFactoryFunc) error MustRegister(T, ProviderFactoryFunc) Get(T) (ProviderFactoryFunc, error) } type registry[T comparable] struct { factories map[T]ProviderFactoryFunc } func (r *registry[T]) Register(name T, factory ProviderFactoryFunc) error { if _, exists := r.factories[name]; exists { return fmt.Errorf("provider '%v' already registered", name) } r.factories[name] = factory return nil } func (r *registry[T]) MustRegister(name T, factory ProviderFactoryFunc) { if err := r.Register(name, factory); err != nil { panic(err) } } func (r *registry[T]) Get(name T) (ProviderFactoryFunc, error) { if factory, exists := r.factories[name]; exists { return factory, nil } return nil, fmt.Errorf("provider '%v' not registered", name) } func newRegistry[T comparable]() Registry[T] { return ®istry[T]{factories: make(map[T]ProviderFactoryFunc)} } var Registries = newRegistry[domain.NotificationProviderType]() ================================================ FILE: internal/notify/notifiers/sp_dingtalkbot.go ================================================ package notifiers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/notifier" "github.com/certimate-go/certimate/pkg/core/notifier/providers/dingtalkbot" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.NotificationProviderTypeDingTalkBot, func(options *ProviderFactoryOptions) (notifier.Provider, error) { credentials := domain.AccessConfigForDingTalkBot{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := dingtalkbot.NewNotifier(&dingtalkbot.NotifierConfig{ WebhookUrl: credentials.WebhookUrl, Secret: credentials.Secret, CustomPayload: credentials.CustomPayload, }) return provider, err }) } ================================================ FILE: internal/notify/notifiers/sp_discordbot.go ================================================ package notifiers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/notifier" "github.com/certimate-go/certimate/pkg/core/notifier/providers/discordbot" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.NotificationProviderTypeDiscordBot, func(options *ProviderFactoryOptions) (notifier.Provider, error) { credentials := domain.AccessConfigForDiscordBot{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := discordbot.NewNotifier(&discordbot.NotifierConfig{ BotToken: credentials.BotToken, ChannelId: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "channelId", credentials.ChannelId), }) return provider, err }) } ================================================ FILE: internal/notify/notifiers/sp_email.go ================================================ package notifiers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/notifier" "github.com/certimate-go/certimate/pkg/core/notifier/providers/email" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.NotificationProviderTypeEmail, func(options *ProviderFactoryOptions) (notifier.Provider, error) { credentials := domain.AccessConfigForEmail{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := email.NewNotifier(&email.NotifierConfig{ SmtpHost: credentials.SmtpHost, SmtpPort: credentials.SmtpPort, SmtpTls: credentials.SmtpTls, Username: credentials.Username, Password: credentials.Password, SenderAddress: credentials.SenderAddress, SenderName: credentials.SenderName, ReceiverAddress: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "receiverAddress", credentials.ReceiverAddress), MessageFormat: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "format", email.MESSAGE_FORMAT_PLAIN), AllowInsecureConnections: credentials.AllowInsecureConnections, }) return provider, err }) } ================================================ FILE: internal/notify/notifiers/sp_larkbot.go ================================================ package notifiers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/notifier" "github.com/certimate-go/certimate/pkg/core/notifier/providers/larkbot" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.NotificationProviderTypeLarkBot, func(options *ProviderFactoryOptions) (notifier.Provider, error) { credentials := domain.AccessConfigForLarkBot{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := larkbot.NewNotifier(&larkbot.NotifierConfig{ WebhookUrl: credentials.WebhookUrl, Secret: credentials.Secret, CustomPayload: credentials.CustomPayload, }) return provider, err }) } ================================================ FILE: internal/notify/notifiers/sp_mattermost.go ================================================ package notifiers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/notifier" "github.com/certimate-go/certimate/pkg/core/notifier/providers/mattermost" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.NotificationProviderTypeMattermost, func(options *ProviderFactoryOptions) (notifier.Provider, error) { credentials := domain.AccessConfigForMattermost{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := mattermost.NewNotifier(&mattermost.NotifierConfig{ ServerUrl: credentials.ServerUrl, Username: credentials.Username, Password: credentials.Password, ChannelId: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "channelId", credentials.ChannelId), }) return provider, err }) } ================================================ FILE: internal/notify/notifiers/sp_slackbot.go ================================================ package notifiers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/notifier" slackbot "github.com/certimate-go/certimate/pkg/core/notifier/providers/slackbot" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.NotificationProviderTypeSlackBot, func(options *ProviderFactoryOptions) (notifier.Provider, error) { credentials := domain.AccessConfigForSlackBot{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := slackbot.NewNotifier(&slackbot.NotifierConfig{ BotToken: credentials.BotToken, ChannelId: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "channelId", credentials.ChannelId), }) return provider, err }) } ================================================ FILE: internal/notify/notifiers/sp_telegrambot.go ================================================ package notifiers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/notifier" "github.com/certimate-go/certimate/pkg/core/notifier/providers/telegrambot" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.NotificationProviderTypeTelegramBot, func(options *ProviderFactoryOptions) (notifier.Provider, error) { credentials := domain.AccessConfigForTelegramBot{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := telegrambot.NewNotifier(&telegrambot.NotifierConfig{ BotToken: credentials.BotToken, ChatId: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "chatId", credentials.ChatId), }) return provider, err }) } ================================================ FILE: internal/notify/notifiers/sp_webhook.go ================================================ package notifiers import ( "fmt" "net/http" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/notifier" "github.com/certimate-go/certimate/pkg/core/notifier/providers/webhook" xhttp "github.com/certimate-go/certimate/pkg/utils/http" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.NotificationProviderTypeWebhook, func(options *ProviderFactoryOptions) (notifier.Provider, error) { credentials := domain.AccessConfigForWebhook{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } mergedHeaders := make(map[string]string) if defaultHeadersString := credentials.HeadersString; defaultHeadersString != "" { h, err := xhttp.ParseHeaders(defaultHeadersString) if err != nil { return nil, fmt.Errorf("failed to parse webhook headers: %w", err) } for key := range h { mergedHeaders[http.CanonicalHeaderKey(key)] = h.Get(key) } } if extendedHeadersString := xmaps.GetString(options.ProviderExtendedConfig, "headers"); extendedHeadersString != "" { h, err := xhttp.ParseHeaders(extendedHeadersString) if err != nil { return nil, fmt.Errorf("failed to parse webhook headers: %w", err) } for key := range h { mergedHeaders[http.CanonicalHeaderKey(key)] = h.Get(key) } } provider, err := webhook.NewNotifier(&webhook.NotifierConfig{ WebhookUrl: credentials.Url, WebhookData: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "webhookData", credentials.DataString), Method: credentials.Method, Headers: mergedHeaders, Timeout: xmaps.GetInt(options.ProviderExtendedConfig, "timeout"), AllowInsecureConnections: credentials.AllowInsecureConnections, }) return provider, err }) } ================================================ FILE: internal/notify/notifiers/sp_wecombot.go ================================================ package notifiers import ( "fmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core/notifier" "github.com/certimate-go/certimate/pkg/core/notifier/providers/wecombot" xmaps "github.com/certimate-go/certimate/pkg/utils/maps" ) func init() { Registries.MustRegister(domain.NotificationProviderTypeWeComBot, func(options *ProviderFactoryOptions) (notifier.Provider, error) { credentials := domain.AccessConfigForWeComBot{} if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } provider, err := wecombot.NewNotifier(&wecombot.NotifierConfig{ WebhookUrl: credentials.WebhookUrl, CustomPayload: credentials.CustomPayload, }) return provider, err }) } ================================================ FILE: internal/notify/service.go ================================================ package notify import ( "context" "fmt" "github.com/certimate-go/certimate/internal/domain/dtos" ) const ( testSubject = "[Certimate] Notification Testing" testMessage = "Welcome to use Certimate!" ) type NotifyService struct { accessRepo accessRepository } func NewNotifyService(accessRepo accessRepository) *NotifyService { return &NotifyService{ accessRepo: accessRepo, } } func (n *NotifyService) TestPush(ctx context.Context, req *dtos.NotifyTestPushReq) (*dtos.NotifyTestPushResp, error) { accessConfig := make(map[string]any) if access, err := n.accessRepo.GetById(ctx, req.AccessId); err != nil { return nil, fmt.Errorf("failed to get access #%s record: %w", req.AccessId, err) } else { if access.Reserve != "notif" { return nil, fmt.Errorf("access #%s is not available for notification", req.AccessId) } accessConfig = access.Config } notifier := NewClient() notifyReq := &SendNotificationRequest{ Provider: req.Provider, ProviderAccessConfig: accessConfig, ProviderExtendedConfig: make(map[string]any), Subject: testSubject, Message: testMessage, } if _, err := notifier.SendNotification(ctx, notifyReq); err != nil { return nil, err } return &dtos.NotifyTestPushResp{}, nil } ================================================ FILE: internal/notify/service_deps.go ================================================ package notify import ( "context" "github.com/certimate-go/certimate/internal/domain" ) type accessRepository interface { GetById(ctx context.Context, id string) (*domain.Access, error) } ================================================ FILE: internal/repository/access.go ================================================ package repository import ( "context" "database/sql" "errors" "github.com/pocketbase/pocketbase/core" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/internal/domain" ) type AccessRepository struct{} func NewAccessRepository() *AccessRepository { return &AccessRepository{} } func (r *AccessRepository) GetById(ctx context.Context, id string) (*domain.Access, error) { record, err := app.GetApp().FindRecordById(domain.CollectionNameAccess, id) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, domain.ErrRecordNotFound } return nil, err } if !record.GetDateTime("deleted").Time().IsZero() { return nil, domain.ErrRecordNotFound } return r.castRecordToModel(record) } func (r *AccessRepository) castRecordToModel(record *core.Record) (*domain.Access, error) { if record == nil { return nil, errors.New("the record is nil") } config := make(map[string]any) if err := record.UnmarshalJSONField("config", &config); err != nil { return nil, errors.New("field 'config' is malformed") } access := &domain.Access{ Meta: domain.Meta{ Id: record.Id, CreatedAt: record.GetDateTime("created").Time(), UpdatedAt: record.GetDateTime("updated").Time(), }, Name: record.GetString("name"), Provider: record.GetString("provider"), Config: config, Reserve: record.GetString("reserve"), } return access, nil } ================================================ FILE: internal/repository/acme_account.go ================================================ package repository import ( "context" "database/sql" "errors" "github.com/go-acme/lego/v4/acme" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/internal/domain" ) type ACMEAccountRepository struct{} func NewACMEAccountRepository() *ACMEAccountRepository { return &ACMEAccountRepository{} } func (r *ACMEAccountRepository) GetByCAAndEmail(ctx context.Context, ca, caDirUrl, email string) (*domain.ACMEAccount, error) { record, err := app.GetApp().FindFirstRecordByFilter( domain.CollectionNameACMEAccount, "ca={:ca} && acmeDirUrl={:acmeDirUrl} && email={:email}", dbx.Params{"ca": ca, "acmeDirUrl": caDirUrl, "email": email}, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, domain.ErrRecordNotFound } return nil, err } return r.castRecordToModel(record) } func (r *ACMEAccountRepository) GetByAcctUrl(ctx context.Context, acctUrl string) (*domain.ACMEAccount, error) { record, err := app.GetApp().FindFirstRecordByFilter( domain.CollectionNameACMEAccount, "acmeAcctUrl={:acmeAcctUrl}", dbx.Params{"acmeAcctUrl": acctUrl}, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, domain.ErrRecordNotFound } return nil, err } return r.castRecordToModel(record) } func (r *ACMEAccountRepository) Save(ctx context.Context, acmeAccount *domain.ACMEAccount) (*domain.ACMEAccount, error) { collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameACMEAccount) if err != nil { return acmeAccount, err } 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("privateKey", acmeAccount.PrivateKey) record.Set("acmeAccount", acmeAccount.ACMEAccount) record.Set("acmeAcctUrl", acmeAccount.ACMEAcctUrl) record.Set("acmeDirUrl", acmeAccount.ACMEDirUrl) 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) { if record == nil { return nil, errors.New("the record is nil") } account := &acme.Account{} if err := record.UnmarshalJSONField("acmeAccount", account); err != nil { return nil, errors.New("field 'acmeAccount' is malformed") } acmeAccount := &domain.ACMEAccount{ Meta: domain.Meta{ Id: record.Id, CreatedAt: record.GetDateTime("created").Time(), UpdatedAt: record.GetDateTime("updated").Time(), }, CA: record.GetString("ca"), Email: record.GetString("email"), PrivateKey: record.GetString("privateKey"), ACMEAccount: account, ACMEAcctUrl: record.GetString("acmeAcctUrl"), ACMEDirUrl: record.GetString("acmeDirUrl"), } return acmeAccount, nil } ================================================ FILE: internal/repository/certificate.go ================================================ package repository import ( "context" "database/sql" "errors" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/internal/domain" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" ) type CertificateRepository struct{} func NewCertificateRepository() *CertificateRepository { return &CertificateRepository{} } func (r *CertificateRepository) ListExpiringSoon(ctx context.Context) ([]*domain.Certificate, error) { records, err := app.GetApp().FindAllRecords( domain.CollectionNameCertificate, dbx.NewExp("validityNotAfter>DATETIME('now')"), dbx.NewExp("validityNotAfter 0 { return ret, errors.Join(errs...) } return ret, nil } func (r *CertificateRepository) castRecordToModel(record *core.Record) (*domain.Certificate, error) { if record == nil { return nil, errors.New("the record is nil") } certificate := &domain.Certificate{ Meta: domain.Meta{ Id: record.Id, CreatedAt: record.GetDateTime("created").Time(), UpdatedAt: record.GetDateTime("updated").Time(), }, Source: domain.CertificateSourceType(record.GetString("source")), SubjectAltNames: record.GetString("subjectAltNames"), SerialNumber: record.GetString("serialNumber"), Certificate: record.GetString("certificate"), PrivateKey: record.GetString("privateKey"), IssuerOrg: record.GetString("issuerOrg"), IssuerCertificate: record.GetString("issuerCertificate"), KeyAlgorithm: domain.CertificateKeyAlgorithmType(record.GetString("keyAlgorithm")), ValidityNotBefore: record.GetDateTime("validityNotBefore").Time(), ValidityNotAfter: record.GetDateTime("validityNotAfter").Time(), ValidityInterval: int32(record.GetInt("validityInterval")), ACMEAcctUrl: record.GetString("acmeAcctUrl"), ACMECertUrl: record.GetString("acmeCertUrl"), IsRenewed: record.GetBool("isRenewed"), IsRevoked: record.GetBool("isRevoked"), WorkflowId: record.GetString("workflowRef"), WorkflowRunId: record.GetString("workflowRunRef"), WorkflowNodeId: record.GetString("workflowNodeId"), } return certificate, nil } ================================================ FILE: internal/repository/settings.go ================================================ package repository import ( "context" "database/sql" "errors" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/internal/domain" "github.com/pocketbase/dbx" ) type SettingsRepository struct{} func NewSettingsRepository() *SettingsRepository { return &SettingsRepository{} } func (r *SettingsRepository) GetByName(ctx context.Context, name string) (*domain.Settings, error) { record, err := app.GetApp().FindFirstRecordByFilter( domain.CollectionNameSettings, "name={:name}", dbx.Params{"name": name}, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, domain.ErrRecordNotFound } return nil, err } content := make(map[string]any) if err := record.UnmarshalJSONField("content", &content); err != nil { return nil, errors.New("field 'content' is malformed") } settings := &domain.Settings{ Meta: domain.Meta{ Id: record.Id, CreatedAt: record.GetDateTime("created").Time(), UpdatedAt: record.GetDateTime("updated").Time(), }, Name: record.GetString("name"), Content: content, } return settings, nil } ================================================ FILE: internal/repository/statistics.go ================================================ package repository import ( "context" "database/sql" "encoding/json" "errors" "fmt" "github.com/pocketbase/dbx" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/internal/domain" ) type StatisticsRepository struct{} func NewStatisticsRepository() *StatisticsRepository { return &StatisticsRepository{} } func (r *StatisticsRepository) Get(ctx context.Context) (*domain.Statistics, error) { statistics := &domain.Statistics{} // 读取设置 var persistenceSettings *domain.SettingsContentForPersistence rsSettings := struct { Content string `db:"content"` }{} if err := app.GetDB(). NewQuery(fmt.Sprintf("SELECT content FROM %s WHERE name = {:name}", domain.CollectionNameSettings)). Bind(dbx.Params{"name": domain.SettingsNamePersistence}). One(&rsSettings); err != nil { if errors.Is(err, sql.ErrNoRows) { persistenceSettings = (domain.SettingsContent{}).AsPersistence() } else { return nil, err } } else { json.Unmarshal([]byte(rsSettings.Content), &persistenceSettings) } // 统计所有证书 rsCertTotal := struct { Total int `db:"total"` }{} if err := app.GetDB(). NewQuery(fmt.Sprintf("SELECT COUNT(*) AS total FROM %s WHERE deleted = ''", domain.CollectionNameCertificate)). One(&rsCertTotal); err != nil { return nil, err } statistics.CertificateTotal = rsCertTotal.Total // 统计即将过期证书 rsCertExpiringSoonTotal := struct { Total int `db:"total"` }{} if err := app.GetDB(). NewQuery(fmt.Sprintf("SELECT COUNT(*) AS total FROM %s WHERE validityNotAfter <= DATETIME('now', '+%d days') AND validityNotAfter > DATETIME('now') AND isRevoked = 0 AND deleted = ''", domain.CollectionNameCertificate, persistenceSettings.CertificatesWarningDaysBeforeExpire)). One(&rsCertExpiringSoonTotal); err != nil { return nil, err } statistics.CertificateExpiringSoon = rsCertExpiringSoonTotal.Total // 统计已过期证书 rsCertExpiredTotal := struct { Total int `db:"total"` }{} if err := app.GetDB(). NewQuery(fmt.Sprintf("SELECT COUNT(*) AS total FROM %s WHERE validityNotAfter <= DATETIME('now') AND deleted = ''", domain.CollectionNameCertificate)). One(&rsCertExpiredTotal); err != nil { return nil, err } statistics.CertificateExpired = rsCertExpiredTotal.Total // 统计所有工作流 rsWorkflowTotal := struct { Total int `db:"total"` }{} if err := app.GetDB(). NewQuery(fmt.Sprintf("SELECT COUNT(*) AS total FROM %s", domain.CollectionNameWorkflow)). One(&rsWorkflowTotal); err != nil { return nil, err } statistics.WorkflowTotal = rsWorkflowTotal.Total // 统计已启用工作流 rsWorkflowEnabledTotal := struct { Total int `db:"total"` }{} if err := app.GetDB(). NewQuery(fmt.Sprintf("SELECT COUNT(*) AS total FROM %s WHERE enabled IS TRUE", domain.CollectionNameWorkflow)). One(&rsWorkflowEnabledTotal); err != nil { return nil, err } statistics.WorkflowEnabled = rsWorkflowEnabledTotal.Total statistics.WorkflowDisabled = rsWorkflowTotal.Total - rsWorkflowEnabledTotal.Total return statistics, nil } ================================================ FILE: internal/repository/workflow.go ================================================ package repository import ( "context" "database/sql" "errors" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/internal/domain" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" ) type WorkflowRepository struct{} func NewWorkflowRepository() *WorkflowRepository { return &WorkflowRepository{} } func (r *WorkflowRepository) ListEnabledScheduled(ctx context.Context) ([]*domain.Workflow, error) { records, err := app.GetApp().FindRecordsByFilter( domain.CollectionNameWorkflow, "enabled={:enabled} && trigger={:trigger}", "-created", 0, 0, dbx.Params{"enabled": true, "trigger": string(domain.WorkflowTriggerTypeScheduled)}, ) if err != nil { return nil, err } workflows := make([]*domain.Workflow, 0) for _, record := range records { workflow, err := r.castRecordToModel(record) if err != nil { return nil, err } workflows = append(workflows, workflow) } return workflows, nil } func (r *WorkflowRepository) GetById(ctx context.Context, id string) (*domain.Workflow, error) { record, err := app.GetApp().FindRecordById(domain.CollectionNameWorkflow, id) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, domain.ErrRecordNotFound } return nil, err } return r.castRecordToModel(record) } func (r *WorkflowRepository) Save(ctx context.Context, workflow *domain.Workflow) (*domain.Workflow, error) { collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflow) if err != nil { return workflow, err } var record *core.Record if workflow.Id == "" { record = core.NewRecord(collection) } else { record, err = app.GetApp().FindRecordById(collection, workflow.Id) if err != nil { if errors.Is(err, sql.ErrNoRows) { return workflow, domain.ErrRecordNotFound } return workflow, err } } record.Set("name", workflow.Name) record.Set("description", workflow.Description) record.Set("trigger", string(workflow.Trigger)) record.Set("triggerCron", workflow.TriggerCron) record.Set("enabled", workflow.Enabled) record.Set("graphDraft", workflow.GraphDraft) record.Set("graphContent", workflow.GraphContent) record.Set("hasDraft", workflow.HasDraft) record.Set("hasContent", workflow.HasContent) record.Set("lastRunRef", workflow.LastRunId) record.Set("lastRunStatus", string(workflow.LastRunStatus)) record.Set("lastRunTime", workflow.LastRunTime) if err := app.GetApp().Save(record); err != nil { return workflow, err } workflow.Id = record.Id workflow.CreatedAt = record.GetDateTime("created").Time() workflow.UpdatedAt = record.GetDateTime("updated").Time() return workflow, nil } func (r *WorkflowRepository) castRecordToModel(record *core.Record) (*domain.Workflow, error) { if record == nil { return nil, errors.New("the record is nil") } graphDraft := &domain.WorkflowGraph{} if err := record.UnmarshalJSONField("graphDraft", graphDraft); err != nil { return nil, errors.New("field 'graphDraft' is malformed") } graphContent := &domain.WorkflowGraph{} if err := record.UnmarshalJSONField("graphContent", graphContent); err != nil { return nil, errors.New("field 'graphContent' is malformed") } workflow := &domain.Workflow{ Meta: domain.Meta{ Id: record.Id, CreatedAt: record.GetDateTime("created").Time(), UpdatedAt: record.GetDateTime("updated").Time(), }, Name: record.GetString("name"), Description: record.GetString("description"), Trigger: domain.WorkflowTriggerType(record.GetString("trigger")), TriggerCron: record.GetString("triggerCron"), Enabled: record.GetBool("enabled"), GraphDraft: graphDraft, GraphContent: graphContent, HasDraft: record.GetBool("hasDraft"), HasContent: record.GetBool("hasContent"), LastRunId: record.GetString("lastRunRef"), LastRunStatus: domain.WorkflowRunStatusType(record.GetString("lastRunStatus")), LastRunTime: record.GetDateTime("lastRunTime").Time(), } return workflow, nil } ================================================ FILE: internal/repository/workflow_log.go ================================================ package repository import ( "context" "database/sql" "errors" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/internal/domain" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" ) type WorkflowLogRepository struct{} func NewWorkflowLogRepository() *WorkflowLogRepository { return &WorkflowLogRepository{} } func (r *WorkflowLogRepository) ListByWorkflowRunId(ctx context.Context, workflowRunId string) ([]*domain.WorkflowLog, error) { records, err := app.GetApp().FindRecordsByFilter( domain.CollectionNameWorkflowLog, "runRef={:runId}", "timestamp", 0, 0, dbx.Params{"runId": workflowRunId}, ) if err != nil { return nil, err } workflowLogs := make([]*domain.WorkflowLog, 0) for _, record := range records { workflowLog, err := r.castRecordToModel(record) if err != nil { return nil, err } workflowLogs = append(workflowLogs, workflowLog) } return workflowLogs, nil } func (r *WorkflowLogRepository) Save(ctx context.Context, workflowLog *domain.WorkflowLog) (*domain.WorkflowLog, error) { collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflowLog) if err != nil { return workflowLog, err } var record *core.Record if workflowLog.Id == "" { record = core.NewRecord(collection) } else { record, err = app.GetApp().FindRecordById(collection, workflowLog.Id) if err != nil { if errors.Is(err, sql.ErrNoRows) { return workflowLog, err } record = core.NewRecord(collection) } } record.Set("workflowRef", workflowLog.WorkflowId) record.Set("runRef", workflowLog.RunId) record.Set("nodeId", workflowLog.NodeId) record.Set("nodeName", workflowLog.NodeName) record.Set("timestamp", workflowLog.TimestampMilli) record.Set("level", workflowLog.Level) record.Set("message", workflowLog.Message) record.Set("data", workflowLog.Data) record.Set("created", workflowLog.CreatedAt) err = app.GetApp().Save(record) if err != nil { return workflowLog, err } workflowLog.Id = record.Id workflowLog.CreatedAt = record.GetDateTime("created").Time() workflowLog.UpdatedAt = record.GetDateTime("updated").Time() return workflowLog, nil } func (r *WorkflowLogRepository) castRecordToModel(record *core.Record) (*domain.WorkflowLog, error) { if record == nil { return nil, errors.New("the record is nil") } logdata := make(map[string]any) if err := record.UnmarshalJSONField("data", &logdata); err != nil { return nil, errors.New("field 'data' is malformed") } workflowLog := &domain.WorkflowLog{ Meta: domain.Meta{ Id: record.Id, CreatedAt: record.GetDateTime("created").Time(), UpdatedAt: record.GetDateTime("updated").Time(), }, WorkflowId: record.GetString("workflowRef"), RunId: record.GetString("runRef"), NodeId: record.GetString("nodeId"), NodeName: record.GetString("nodeName"), TimestampMilli: int64(record.GetInt("timestamp")), Level: int32(record.GetInt("level")), Message: record.GetString("message"), Data: logdata, } return workflowLog, nil } ================================================ FILE: internal/repository/workflow_output.go ================================================ package repository import ( "context" "database/sql" "errors" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/internal/domain" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" ) type WorkflowOutputRepository struct{} func NewWorkflowOutputRepository() *WorkflowOutputRepository { return &WorkflowOutputRepository{} } func (r *WorkflowOutputRepository) GetByWorkflowIdAndNodeId(ctx context.Context, workflowId string, workflowNodeId string) (*domain.WorkflowOutput, error) { records, err := app.GetApp().FindRecordsByFilter( domain.CollectionNameWorkflowOutput, "workflowRef={:workflowId} && nodeId={:nodeId}", "-created", 1, 0, dbx.Params{"workflowId": workflowId}, dbx.Params{"nodeId": workflowNodeId}, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, domain.ErrRecordNotFound } return nil, err } if len(records) == 0 { return nil, domain.ErrRecordNotFound } return r.castRecordToModel(records[0]) } func (r *WorkflowOutputRepository) GetByWorkflowRunIdAndNodeId(ctx context.Context, workflowRunId string, workflowNodeId string) (*domain.WorkflowOutput, error) { records, err := app.GetApp().FindRecordsByFilter( domain.CollectionNameWorkflowOutput, "runRef={:workflowRunId} && nodeId={:nodeId}", "-created", 1, 0, dbx.Params{"workflowRunId": workflowRunId}, dbx.Params{"nodeId": workflowNodeId}, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, domain.ErrRecordNotFound } return nil, err } if len(records) == 0 { return nil, domain.ErrRecordNotFound } 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) castRecordToModel(record *core.Record) (*domain.WorkflowOutput, error) { if record == nil { return nil, errors.New("the record is nil") } nodeConfig := make(domain.WorkflowNodeConfig) if err := record.UnmarshalJSONField("nodeConfig", &nodeConfig); err != nil { return nil, errors.New("field 'nodeConfig' is malformed") } outputs := make([]*domain.WorkflowOutputEntry, 0) if err := record.UnmarshalJSONField("outputs", &outputs); err != nil { return nil, errors.New("field 'outputs' is malformed") } workflowOutput := &domain.WorkflowOutput{ Meta: domain.Meta{ Id: record.Id, CreatedAt: record.GetDateTime("created").Time(), UpdatedAt: record.GetDateTime("updated").Time(), }, WorkflowId: record.GetString("workflowRef"), RunId: record.GetString("runRef"), NodeId: record.GetString("nodeId"), NodeConfig: nodeConfig, Outputs: outputs, Succeeded: record.GetBool("succeeded"), } return workflowOutput, nil } func (r *WorkflowOutputRepository) saveRecord(workflowOutput *domain.WorkflowOutput) (*core.Record, error) { collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameWorkflowOutput) if err != nil { return nil, err } var record *core.Record if workflowOutput.Id == "" { record = core.NewRecord(collection) } else { record, err = app.GetApp().FindRecordById(collection, workflowOutput.Id) if err != nil { return record, err } } record.Set("workflowRef", workflowOutput.WorkflowId) record.Set("runRef", workflowOutput.RunId) record.Set("nodeId", workflowOutput.NodeId) record.Set("nodeConfig", workflowOutput.NodeConfig) record.Set("outputs", workflowOutput.Outputs) record.Set("succeeded", workflowOutput.Succeeded) if err := app.GetApp().Save(record); err != nil { return record, err } return record, nil } ================================================ FILE: internal/repository/workflow_run.go ================================================ package repository import ( "context" "database/sql" "errors" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/internal/domain" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" ) 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) } } record.Set("workflowRef", 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("graph", workflowRun.Graph) record.Set("error", workflowRun.Error) err = app.GetApp().Save(record) if err != nil { return workflowRun, err } workflowRun.Id = record.Id workflowRun.CreatedAt = record.GetDateTime("created").Time() workflowRun.UpdatedAt = record.GetDateTime("updated").Time() return workflowRun, nil } func (r *WorkflowRunRepository) SaveWithCascading(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("workflowRef", 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("graph", workflowRun.Graph) 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("lastRunRef") { 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("lastRunRef", 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) DeleteWhere(ctx context.Context, exprs ...dbx.Expression) (int, error) { records, err := app.GetApp().FindAllRecords(domain.CollectionNameWorkflowRun, exprs...) if err != nil { return 0, nil } var ret int var errs []error for _, record := range records { if err := app.GetApp().Delete(record); err != nil { errs = append(errs, err) } else { ret++ } } if len(errs) > 0 { return ret, errors.Join(errs...) } return ret, nil } func (r *WorkflowRunRepository) castRecordToModel(record *core.Record) (*domain.WorkflowRun, error) { if record == nil { return nil, errors.New("the record is nil") } graph := &domain.WorkflowGraph{} if err := record.UnmarshalJSONField("graph", &graph); err != nil { return nil, errors.New("field 'graph' is malformed") } workflowRun := &domain.WorkflowRun{ Meta: domain.Meta{ Id: record.Id, CreatedAt: record.GetDateTime("created").Time(), UpdatedAt: record.GetDateTime("updated").Time(), }, WorkflowId: record.GetString("workflowRef"), Status: domain.WorkflowRunStatusType(record.GetString("status")), Trigger: domain.WorkflowTriggerType(record.GetString("trigger")), StartedAt: record.GetDateTime("startedAt").Time(), EndedAt: record.GetDateTime("endedAt").Time(), Graph: graph, Error: record.GetString("error"), } return workflowRun, nil } ================================================ FILE: internal/rest/handlers/certificates.go ================================================ package handlers import ( "context" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tools/router" "github.com/certimate-go/certimate/internal/domain/dtos" "github.com/certimate-go/certimate/internal/rest/resp" ) type certificateService interface { DownloadCertificate(ctx context.Context, req *dtos.CertificateDownloadReq) (*dtos.CertificateDownloadResp, error) RevokeCertificate(ctx context.Context, req *dtos.CertificateRevokeReq) (*dtos.CertificateRevokeResp, error) } type CertificatesHandler struct { service certificateService } func NewCertificatesHandler(router *router.RouterGroup[*core.RequestEvent], service certificateService) { handler := &CertificatesHandler{ service: service, } group := router.Group("/certificates") group.POST("/{certificateId}/download", handler.downloadCertificate) group.POST("/{certificateId}/revoke", handler.revokeCertificate) group.POST("/{certificateId}/archive", handler.downloadCertificate) // 兼容旧版 } func (handler *CertificatesHandler) downloadCertificate(e *core.RequestEvent) error { req := &dtos.CertificateDownloadReq{} req.CertificateId = e.Request.PathValue("certificateId") if err := e.BindBody(req); err != nil { return resp.Err(e, err) } res, err := handler.service.DownloadCertificate(e.Request.Context(), req) if err != nil { return resp.Err(e, err) } return resp.Ok(e, res) } func (handler *CertificatesHandler) revokeCertificate(e *core.RequestEvent) error { req := &dtos.CertificateRevokeReq{} req.CertificateId = e.Request.PathValue("certificateId") if err := e.BindBody(req); err != nil { return resp.Err(e, err) } res, err := handler.service.RevokeCertificate(e.Request.Context(), req) if err != nil { return resp.Err(e, err) } return resp.Ok(e, res) } ================================================ FILE: internal/rest/handlers/notifications.go ================================================ package handlers import ( "context" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tools/router" "github.com/certimate-go/certimate/internal/domain/dtos" "github.com/certimate-go/certimate/internal/rest/resp" ) type notifyService interface { TestPush(ctx context.Context, req *dtos.NotifyTestPushReq) (*dtos.NotifyTestPushResp, error) } type NotificationsHandler struct { service notifyService } func NewNotificationsHandler(router *router.RouterGroup[*core.RequestEvent], service notifyService) { handler := &NotificationsHandler{ service: service, } group := router.Group("/notifications") group.POST("/test", handler.test) } func (handler *NotificationsHandler) test(e *core.RequestEvent) error { req := &dtos.NotifyTestPushReq{} if err := e.BindBody(req); err != nil { return resp.Err(e, err) } res, err := handler.service.TestPush(e.Request.Context(), req) if err != nil { return resp.Err(e, err) } return resp.Ok(e, res) } ================================================ FILE: internal/rest/handlers/statistics.go ================================================ package handlers import ( "context" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tools/router" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/internal/rest/resp" ) type statisticsService interface { Get(ctx context.Context) (*domain.Statistics, error) } type StatisticsHandler struct { service statisticsService } func NewStatisticsHandler(router *router.RouterGroup[*core.RequestEvent], service statisticsService) { handler := &StatisticsHandler{ service: service, } router.GET("/statistics", handler.get) router.GET("/statistics/get", handler.get) // 兼容旧版 } func (handler *StatisticsHandler) get(e *core.RequestEvent) error { res, err := handler.service.Get(e.Request.Context()) if err != nil { return resp.Err(e, err) } return resp.Ok(e, res) } ================================================ FILE: internal/rest/handlers/workflows.go ================================================ package handlers import ( "context" "errors" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tools/router" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/internal/domain/dtos" "github.com/certimate-go/certimate/internal/rest/resp" ) type workflowService interface { GetStatistics(ctx context.Context) (*dtos.WorkflowStatisticsResp, error) StartRun(ctx context.Context, req *dtos.WorkflowStartRunReq) (*dtos.WorkflowStartRunResp, error) CancelRun(ctx context.Context, req *dtos.WorkflowCancelRunReq) (*dtos.WorkflowCancelRunResp, error) Shutdown(ctx context.Context) } type WorkflowsHandler struct { service workflowService } func NewWorkflowsHandler(router *router.RouterGroup[*core.RequestEvent], service workflowService) { handler := &WorkflowsHandler{ service: service, } group := router.Group("/workflows") group.GET("/stats", handler.getStatistics) group.POST("/{workflowId}/runs", handler.startRun) group.POST("/{workflowId}/runs/{runId}/cancel", handler.cancelRun) } func (handler *WorkflowsHandler) getStatistics(e *core.RequestEvent) error { res, err := handler.service.GetStatistics(e.Request.Context()) if err != nil { return resp.Err(e, err) } return resp.Ok(e, res) } func (handler *WorkflowsHandler) startRun(e *core.RequestEvent) error { req := &dtos.WorkflowStartRunReq{} req.WorkflowId = e.Request.PathValue("workflowId") if err := e.BindBody(req); err != nil { return resp.Err(e, err) } if req.RunTrigger != domain.WorkflowTriggerTypeManual { return resp.Err(e, errors.New("invalid parameters: the value of 'trigger' must be 'manual'")) } res, err := handler.service.StartRun(e.Request.Context(), req) if err != nil { return resp.Err(e, err) } return resp.Ok(e, res) } func (handler *WorkflowsHandler) cancelRun(e *core.RequestEvent) error { req := &dtos.WorkflowCancelRunReq{} req.WorkflowId = e.Request.PathValue("workflowId") req.RunId = e.Request.PathValue("runId") res, err := handler.service.CancelRun(e.Request.Context(), req) if err != nil { return resp.Err(e, err) } return resp.Ok(e, res) } ================================================ FILE: internal/rest/resp/resp.go ================================================ package resp import ( "net/http" "github.com/pocketbase/pocketbase/core" "github.com/certimate-go/certimate/internal/domain" ) type Response struct { Code int `json:"code"` Msg string `json:"msg"` Data interface{} `json:"data"` } func Ok(e *core.RequestEvent, data interface{}) error { rs := &Response{ Code: 0, Msg: "success", Data: data, } return e.JSON(http.StatusOK, rs) } func Err(e *core.RequestEvent, err error) error { code := 500 xerr, ok := err.(*domain.Error) if ok { code = xerr.Code } rs := &Response{ Code: code, Msg: err.Error(), Data: nil, } return e.JSON(http.StatusOK, rs) } ================================================ FILE: internal/rest/routes/routes.go ================================================ package routes import ( "github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tools/router" "github.com/certimate-go/certimate/internal/certificate" "github.com/certimate-go/certimate/internal/notify" "github.com/certimate-go/certimate/internal/repository" "github.com/certimate-go/certimate/internal/rest/handlers" "github.com/certimate-go/certimate/internal/statistics" "github.com/certimate-go/certimate/internal/workflow" ) var ( certificateSvc *certificate.CertificateService workflowSvc *workflow.WorkflowService statisticsSvc *statistics.StatisticsService notifySvc *notify.NotifyService ) func BindRouter(router *router.Router[*core.RequestEvent]) { accessRepo := repository.NewAccessRepository() workflowRepo := repository.NewWorkflowRepository() workflowRunRepo := repository.NewWorkflowRunRepository() acmeAccountRepo := repository.NewACMEAccountRepository() certificateRepo := repository.NewCertificateRepository() settingsRepo := repository.NewSettingsRepository() statisticsRepo := repository.NewStatisticsRepository() certificateSvc = certificate.NewCertificateService(acmeAccountRepo, certificateRepo, settingsRepo) workflowSvc = workflow.NewWorkflowService(workflowRepo, workflowRunRepo, settingsRepo) statisticsSvc = statistics.NewStatisticsService(statisticsRepo) notifySvc = notify.NewNotifyService(accessRepo) group := router.Group("/api") group.Bind(apis.RequireSuperuserAuth()) handlers.NewCertificatesHandler(group, certificateSvc) handlers.NewWorkflowsHandler(group, workflowSvc) handlers.NewStatisticsHandler(group, statisticsSvc) handlers.NewNotificationsHandler(group, notifySvc) } ================================================ FILE: internal/scheduler/certificate.go ================================================ package scheduler import "context" type certificateService interface { InitSchedule(ctx context.Context) error } func InitCertificateScheduler(service certificateService) error { return service.InitSchedule(context.Background()) } ================================================ FILE: internal/scheduler/scheduler.go ================================================ package scheduler import ( "log/slog" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/internal/certificate" "github.com/certimate-go/certimate/internal/repository" "github.com/certimate-go/certimate/internal/workflow" ) func Setup() { workflowRepo := repository.NewWorkflowRepository() workflowRunRepo := repository.NewWorkflowRunRepository() acmeAccountRepo := repository.NewACMEAccountRepository() certificateRepo := repository.NewCertificateRepository() settingsRepo := repository.NewSettingsRepository() workflowSvc := workflow.NewWorkflowService(workflowRepo, workflowRunRepo, settingsRepo) certificateSvc := certificate.NewCertificateService(acmeAccountRepo, certificateRepo, settingsRepo) if err := InitWorkflowScheduler(workflowSvc); err != nil { app.GetLogger().Error("failed to init workflow scheduler", slog.Any("error", err)) } if err := InitCertificateScheduler(certificateSvc); err != nil { app.GetLogger().Error("failed to init certificate scheduler", slog.Any("error", err)) } } ================================================ FILE: internal/scheduler/workflow.go ================================================ package scheduler import "context" type workflowService interface { InitSchedule(ctx context.Context) error } func InitWorkflowScheduler(service workflowService) error { return service.InitSchedule(context.Background()) } ================================================ FILE: internal/statistics/service.go ================================================ package statistics import ( "context" "github.com/certimate-go/certimate/internal/domain" ) type StatisticsService struct { statRepo statisticsRepository } func NewStatisticsService(statRepo statisticsRepository) *StatisticsService { return &StatisticsService{ statRepo: statRepo, } } func (s *StatisticsService) Get(ctx context.Context) (*domain.Statistics, error) { return s.statRepo.Get(ctx) } ================================================ FILE: internal/statistics/service_deps.go ================================================ package statistics import ( "context" "github.com/certimate-go/certimate/internal/domain" ) type statisticsRepository interface { Get(ctx context.Context) (*domain.Statistics, error) } ================================================ FILE: internal/tools/mproc/receiver.go ================================================ package mproc import ( "context" "encoding/hex" "encoding/json" "errors" "fmt" "os" xcrypto "github.com/certimate-go/certimate/pkg/utils/crypto" ) type Receiver[TIn any, TOut any] interface { Receive(infile, outfile, enckey string) error ReceiveWithContext(ctx context.Context, infile, outfile, enckey string) error } type ReceiverHandler[TIn any, TOut any] func(ctx context.Context, params *TIn) (*TOut, error) type receiver[TIn any, TOut any] struct { handler ReceiverHandler[TIn, TOut] } func (r *receiver[TIn, TOut]) Receive(infile, outfile, enckey string) error { return r.ReceiveWithContext(context.Background(), infile, outfile, enckey) } func (r *receiver[TIn, TOut]) ReceiveWithContext(ctx context.Context, infile, outfile, enckey string) error { if infile == "" { return errors.New("mproc: missing or invalid input file") } if outfile == "" { return errors.New("mproc: missing or invalid output file") } if enckey == "" { return errors.New("mproc: missing or invalid encryption key") } aesKey, err := hex.DecodeString(enckey) if err != nil { return fmt.Errorf("mproc: missing or invalid encryption key: %w", err) } aesCryptor := xcrypto.NewAESCryptor(aesKey) // 读取输入 inCipherData, err := os.ReadFile(infile) if err != nil { return fmt.Errorf("mproc: failed to read input file: %w", err) } // 解密输入 inPlainData, err := aesCryptor.CBCDecrypt(inCipherData) if err != nil { return fmt.Errorf("mproc: failed to decrypt input data: %w", err) } // 反序列化输入 var inData TIn if err := json.Unmarshal(inPlainData, &inData); err != nil { return fmt.Errorf("mproc: failed to unmarshal input data: %w", err) } // 处理 outData, err := r.handler(ctx, &inData) if err != nil { return err } // 序列化输出 outPlainData, err := json.Marshal(outData) if err != nil { return fmt.Errorf("mproc: failed to marshal output data: %w", err) } // 加密输出 outCipherData, err := aesCryptor.CBCEncrypt(outPlainData) if err != nil { return fmt.Errorf("mproc: failed to encrypt output data: %w", err) } // 写入输出 if err := os.WriteFile(outfile, outCipherData, 0o644); err != nil { return fmt.Errorf("mproc: failed to write output file: %w", err) } return nil } // 创建并返回一个多进程指令接收器。 // // 入参: // - handler: 多进程指令处理函数。 // // 出参: // - 多进程指令接收器。 func NewReceiver[TIn any, TOut any](handler ReceiverHandler[TIn, TOut]) Receiver[TIn, TOut] { return &receiver[TIn, TOut]{handler: handler} } ================================================ FILE: internal/tools/mproc/sender.go ================================================ package mproc import ( "context" "crypto/rand" "encoding/hex" "encoding/json" "errors" "fmt" "log/slog" "os" "strings" "github.com/go-cmd/cmd" xcrypto "github.com/certimate-go/certimate/pkg/utils/crypto" ) type Sender[TIn any, TOut any] interface { Send(params *TIn) (*TOut, error) SendWithContext(ctx context.Context, params *TIn) (*TOut, error) } type sender[TIn any, TOut any] struct { command string logger *slog.Logger } func (s *sender[TIn, TOut]) Send(params *TIn) (*TOut, error) { return s.SendWithContext(context.Background(), params) } func (s *sender[TIn, TOut]) SendWithContext(ctx context.Context, params *TIn) (*TOut, error) { // 生成随机密钥 aesKey := make([]byte, 32) if _, err := rand.Read(aesKey); err != nil { return nil, fmt.Errorf("mproc: failed to generate aes key: %w", err) } aesCryptor := xcrypto.NewAESCryptor(aesKey) // 准备临时输入文件 tempIn, err := os.CreateTemp("", "certimate.mprocin_*.tmp") if err != nil { return nil, fmt.Errorf("mproc: failed to create temp input file: %w", err) } else { inPlainData, err := json.Marshal(params) if err != nil { return nil, fmt.Errorf("mproc: failed to marshal input data: %w", err) } inCipherData, err := aesCryptor.CBCEncrypt(inPlainData) if err != nil { return nil, fmt.Errorf("mproc: failed to encrypt input data: %w", err) } if _, err := tempIn.Write(inCipherData); err != nil { return nil, fmt.Errorf("mproc: failed to write input file: %w", err) } tempIn.Close() } defer os.Remove(tempIn.Name()) // 准备临时输出文件 tempOut, err := os.CreateTemp("", "certimate.mprocout_*.tmp") if err != nil { return nil, fmt.Errorf("mproc: failed to create temp output file: %w", err) } else { tempOut.Close() } defer os.Remove(tempOut.Name()) // 准备临时错误文件 tempErr, err := os.CreateTemp("", "certimate.mprocerr_*.tmp") if err != nil { return nil, fmt.Errorf("mproc: failed to create temp error file: %w", err) } else { tempErr.Close() } defer os.Remove(tempOut.Name()) // 初始化子进程 done := make(chan struct{}) mcmd := cmd.NewCmdOptions(cmd.Options{Buffered: false, Streaming: true}, s.getEntrypoint(), "intercmd", s.command, "--in", tempIn.Name(), "--out", tempOut.Name(), "--err", tempErr.Name(), "--enckey", hex.EncodeToString(aesKey), ) go func() { defer close(done) for mcmd.Stdout != nil || mcmd.Stderr != nil { select { case line, open := <-mcmd.Stdout: { if !open { mcmd.Stdout = nil continue } if s.logger != nil { print := s.logger.Info // split log level prefix for those vendor packages: // - github.com/go-acme/lego: INFO, WARN if strings.HasPrefix(line, "[INFO] ") { line = strings.TrimPrefix(line, "[INFO] ") print = s.logger.Info } else if strings.HasPrefix(line, "[WARN] ") { line = strings.TrimPrefix(line, "[WARN] ") print = s.logger.Warn } print(line) } } case line, open := <-mcmd.Stderr: { if !open { mcmd.Stderr = nil continue } if s.logger != nil { print := s.logger.Error // split log level prefix for those vendor packages: // - github.com/nrdcg/desec: DEBUG if strings.Contains(line, "[DEBUG] ") { line = strings.SplitN(line, "[DEBUG] ", 2)[1] print = s.logger.Debug } print(line) } } } } }() // 等待子进程退出 <-mcmd.Start() <-done if err := mcmd.Status().Error; err != nil { return nil, fmt.Errorf("mproc: failed to exec child process: %w", err) } // 读取输出 outCipherData, err := os.ReadFile(tempOut.Name()) if err != nil { return nil, fmt.Errorf("mproc: failed to read output file: %w", err) } else { errData, _ := os.ReadFile(tempErr.Name()) if len(errData) > 0 { return nil, errors.New(string(errData)) } } // 解密输出 outPlainData, err := aesCryptor.CBCDecrypt(outCipherData) if err != nil { return nil, fmt.Errorf("mproc: failed to decrypt output data: %w", err) } // 反序列化输出 var outData TOut if err := json.Unmarshal(outPlainData, &outData); err != nil { return nil, fmt.Errorf("mproc: failed to unmarshal output data: %w", err) } return &outData, nil } func (s *sender[TIn, TOut]) getEntrypoint() string { executable, err := os.Executable() if err != nil { executable = os.Args[0] } return executable } // 创建并返回一个多进程指令发送器。 // // 入参: // - command: 多进程指令命令。需要先注册为 `intercmd [command]` 命令行。 // - logger: 日志记录器,将重定向多进程的标准输出流和标准错误流到该日志记录器中。 // // 出参: // - 多进程指令发送器。 func NewSender[TIn any, TOut any](command string, logger *slog.Logger) Sender[TIn, TOut] { return &sender[TIn, TOut]{command: command, logger: logger} } ================================================ FILE: internal/tools/s3/client.go ================================================ package s3 import ( "bytes" "context" "fmt" "io" "regexp" "strings" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" "github.com/samber/lo" xhttp "github.com/certimate-go/certimate/pkg/utils/http" xtls "github.com/certimate-go/certimate/pkg/utils/tls" ) type Client struct { cli *minio.Client } func NewClient(config *Config) (*Client, error) { if config == nil { return nil, fmt.Errorf("the configuration of S3 client is nil") } client, err := createS3Client(config) if err != nil { return nil, err } return &Client{cli: client}, nil } func (c *Client) PutObject(ctx context.Context, bucket, key string, reader io.Reader, size int64) error { putOpts := minio.PutObjectOptions{ DisableMultipart: true, } _, err := c.cli.PutObject(ctx, bucket, key, reader, size, putOpts) if err != nil { return fmt.Errorf("s3: failed to put object: %w", err) } return nil } func (c *Client) PutObjectString(ctx context.Context, bucket, key string, data string) error { reader := strings.NewReader(data) return c.PutObject(ctx, bucket, key, reader, reader.Size()) } func (c *Client) PutObjectBytes(ctx context.Context, bucket, key string, data []byte) error { reader := bytes.NewReader(data) return c.PutObject(ctx, bucket, key, reader, reader.Size()) } func (c *Client) RemoveObject(ctx context.Context, bucket, key string) error { removeOpts := minio.RemoveObjectOptions{} err := c.cli.RemoveObject(ctx, bucket, key, removeOpts) if err != nil { return fmt.Errorf("s3: failed to remove object: %w", err) } return nil } func createS3Client(config *Config) (*minio.Client, error) { var clientCred *credentials.Credentials switch config.SignatureVersion { case "", SignatureV4: clientCred = credentials.NewStaticV4(config.AccessKey, config.SecretKey, "") case SignatureV2: clientCred = credentials.NewStaticV2(config.AccessKey, config.SecretKey, "") default: return nil, fmt.Errorf("s3: unsupported signature version: '%s'", config.SignatureVersion) } endpoint, secure := resolveEndpoint(config.Endpoint) clientOpts := &minio.Options{ Creds: clientCred, Region: config.Region, BucketLookup: lo.If(config.UsePathStyle, minio.BucketLookupPath).Else(minio.BucketLookupDNS), Secure: secure, } if secure && config.SkipTlsVerify { transport := xhttp.NewDefaultTransport() transport.TLSClientConfig = xtls.NewInsecureConfig() clientOpts.Transport = transport } client, err := minio.New(endpoint, clientOpts) if err != nil { return nil, fmt.Errorf("s3: %w", err) } return client, nil } func resolveEndpoint(endpoint string) (string, bool) { var secure bool var result string reScheme := regexp.MustCompile(`^([^:]+)://`) if reScheme.MatchString(endpoint) { temp := strings.Split(endpoint, "://") scheme := temp[0] result = temp[1] secure = strings.EqualFold(scheme, "https") } else { result = endpoint secure = true } return result, secure } ================================================ FILE: internal/tools/s3/config.go ================================================ package s3 const ( SignatureV2 = "v2" SignatureV4 = "v4" ) const ( defaultSignatureVersion = SignatureV4 ) type Config struct { Endpoint string AccessKey string SecretKey string SignatureVersion string UsePathStyle bool Region string SkipTlsVerify bool } func NewDefaultConfig() *Config { return &Config{ SignatureVersion: defaultSignatureVersion, } } ================================================ FILE: internal/tools/smtp/client.go ================================================ package smtp import ( "context" "errors" "fmt" "time" "github.com/wneessen/go-mail" xtls "github.com/certimate-go/certimate/pkg/utils/tls" ) type Client struct { cli *mail.Client } func NewClient(config *Config) (*Client, error) { if config == nil { return nil, fmt.Errorf("the configuration of SMTP client is nil") } client, err := createSmtpClient(config) if err != nil { return nil, err } return &Client{cli: client}, nil } func (c *Client) Close() error { return c.cli.Close() } func (c *Client) Send(ctx context.Context, msg *Message) error { if err := c.cli.DialAndSendWithContext(ctx, msg); err != nil { errShouldBeIgnored := false // REF: https://github.com/wneessen/go-mail/issues/463 var sendErr *mail.SendError if errors.As(err, &sendErr) { if sendErr.Reason == mail.ErrSMTPReset { errShouldBeIgnored = true } } if !errShouldBeIgnored { return fmt.Errorf("smtp: %w", err) } } return nil } func createSmtpClient(config *Config) (*mail.Client, error) { clientOptions := []mail.Option{ mail.WithSMTPAuth(mail.SMTPAuthAutoDiscover), mail.WithUsername(config.Username), mail.WithPassword(config.Password), mail.WithTimeout(time.Second * 30), } if config.Port == 0 { if config.UseSsl { clientOptions = append(clientOptions, mail.WithPort(mail.DefaultPortSSL)) } else { clientOptions = append(clientOptions, mail.WithPort(mail.DefaultPort)) } } else { clientOptions = append(clientOptions, mail.WithPort(config.Port)) } if config.UseSsl { tlsConfig := xtls.NewCompatibleConfig() if config.SkipTlsVerify { tlsConfig.InsecureSkipVerify = true } else { tlsConfig.ServerName = config.Host } clientOptions = append(clientOptions, mail.WithSSL()) clientOptions = append(clientOptions, mail.WithTLSConfig(tlsConfig)) clientOptions = append(clientOptions, mail.WithTLSPolicy(mail.TLSMandatory)) } else { clientOptions = append(clientOptions, mail.WithTLSPolicy(mail.TLSOpportunistic)) } client, err := mail.NewClient(config.Host, clientOptions...) if err != nil { return nil, fmt.Errorf("smtp: %w", err) } client.ErrorHandlerRegistry.RegisterHandler("smtp.qq.com", "QUIT", &wQQMailQuitErrorHandler{}) return client, nil } ================================================ FILE: internal/tools/smtp/config.go ================================================ package smtp const ( defaultPort int = 25 ) type Config struct { Host string Port int Username string Password string UseSsl bool SkipTlsVerify bool } func NewDefaultConfig() *Config { return &Config{ Port: defaultPort, } } ================================================ FILE: internal/tools/smtp/errhandler.go ================================================ package smtp import ( "bytes" "errors" "io" "net/textproto" ) // REF: https://github.com/wneessen/go-mail/wiki/Error-Registry type wQQMailQuitErrorHandler struct{} func (q *wQQMailQuitErrorHandler) HandleError(_, _ string, conn *textproto.Conn, err error) error { var tpErr textproto.ProtocolError if errors.As(err, &tpErr) { if len(tpErr.Error()) < 16 { return err } if !bytes.Equal([]byte(tpErr.Error()[16:]), []byte("\x00\x00\x00\x1a\x00\x00\x00")) { return err } _, _ = io.ReadFull(conn.R, make([]byte, 8)) return nil } return err } ================================================ FILE: internal/tools/smtp/message.go ================================================ package smtp import ( "github.com/wneessen/go-mail" ) type Message = mail.Msg func NewMessage() *Message { return mail.NewMsg() } type MIMEType = mail.ContentType const ( MIMETypeTextHTML MIMEType = mail.TypeTextHTML MIMETypeTextPlain MIMEType = mail.TypeTextPlain ) ================================================ FILE: internal/tools/ssh/auth.go ================================================ package ssh type AuthMethodType string const ( AuthMethodTypeNone AuthMethodType = "none" AuthMethodTypePassword AuthMethodType = "password" AuthMethodTypeKey AuthMethodType = "key" ) ================================================ FILE: internal/tools/ssh/client.go ================================================ package ssh import ( "errors" "fmt" "net" "strconv" "strings" "github.com/samber/lo" "golang.org/x/crypto/ssh" ) type Client struct { conns []net.Conn clis []*ssh.Client } func NewClient(config *Config) (*Client, error) { if config == nil { return nil, fmt.Errorf("the configuration of SSH client is nil") } conns, clis, err := createConnsAndSshClients(config) if err != nil { for i := len(clis) - 1; i >= 0; i-- { clis[i].Close() } for i := len(conns) - 1; i >= 0; i-- { conns[i].Close() } return nil, err } return &Client{conns: conns, clis: clis}, nil } func (c *Client) Close() error { errs := make([]error, 0) for i := len(c.clis) - 1; i >= 0; i-- { cli := c.clis[i] if err := cli.Close(); err != nil { errs = append(errs, err) } } for i := len(c.conns) - 1; i >= 0; i-- { conn := c.conns[i] if err := conn.Close(); err != nil { errs = append(errs, err) } } if len(errs) == 0 { return nil } else if len(errs) == 1 { return errs[0] } else { return errors.Join(errs...) } } func (c *Client) GetClient() *ssh.Client { if len(c.clis) == 0 { return nil } return c.clis[len(c.clis)-1] } func createConnsAndSshClients(config *Config) (conns []net.Conn, clis []*ssh.Client, err error) { conns = make([]net.Conn, 0) clis = make([]*ssh.Client, 0) var targetConn net.Conn if len(config.JumpServers) > 0 { var jumpCli *ssh.Client for i, jumpConfig := range config.JumpServers { var jumpConn net.Conn if jumpCli == nil { jumpConn, err = net.Dial("tcp", resolveAddr(jumpConfig.Host, jumpConfig.Port)) } else { jumpConn, err = jumpCli.Dial("tcp", resolveAddr(jumpConfig.Host, jumpConfig.Port)) } if err != nil { err = fmt.Errorf("ssh: failed to connect to jump server [%d]: %w", i+1, err) return } conns = append(conns, jumpConn) jumpCli, err = createSshClientWithConn(&jumpConfig, jumpConn) if err != nil { err = fmt.Errorf("ssh: failed to create jump server SSH client[%d]: %w", i+1, err) return } clis = append(clis, jumpCli) } // 通过跳板机发起 TCP 连接到目标服务器 targetConn, err = jumpCli.Dial("tcp", resolveAddr(config.Host, config.Port)) if err != nil { err = fmt.Errorf("ssh: failed to connect to target server: %w", err) return } conns = append(conns, targetConn) } else { // 直接发起 TCP 连接到目标服务器 targetConn, err = net.Dial("tcp", resolveAddr(config.Host, config.Port)) if err != nil { err = fmt.Errorf("ssh: failed to connect to target server: %w", err) return } conns = append(conns, targetConn) } // 创建 SSH 客户端 targetCli, err := createSshClientWithConn(&config.ServerConfig, targetConn) if err != nil { return nil, nil, fmt.Errorf("ssh: failed to create SSH client: %w", err) } clis = append(clis, targetCli) return conns, clis, nil } func createSshClientWithConn(config *ServerConfig, conn net.Conn) (*ssh.Client, error) { if conn == nil { return nil, fmt.Errorf("ssh: nil conn") } authMethodType := lo. If(string(config.AuthMethod) != "", config.AuthMethod). ElseIf(config.Key != "", AuthMethodTypeKey). ElseIf(config.Password != "", AuthMethodTypePassword). Else(AuthMethodTypeNone) authMethods := make([]ssh.AuthMethod, 0) switch authMethodType { case AuthMethodTypeNone: { if config.Username == "" { return nil, fmt.Errorf("ssh: unset username") } } case AuthMethodTypePassword: { if config.Username == "" { return nil, fmt.Errorf("ssh: unset username") } if config.Password == "" { return nil, fmt.Errorf("ssh: unset password") } password := config.Password authMethods = append(authMethods, ssh.Password(password)) authMethods = append(authMethods, ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) { answers := make([]string, len(questions)) if len(answers) == 0 { return answers, nil } for i, question := range questions { question = strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(question), ":")) if strings.EqualFold(question, "Password") { answers[i] = password return answers, nil } } return nil, fmt.Errorf("unexpected keyboard interactive question '%s'", strings.Join(questions, ", ")) })) } case AuthMethodTypeKey: { if config.Username == "" { return nil, fmt.Errorf("ssh: unset username") } if config.Key == "" { return nil, fmt.Errorf("ssh: unset key") } key := config.Key keyPassphrase := config.KeyPassphrase var signer ssh.Signer var err error if keyPassphrase != "" { signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(key), []byte(keyPassphrase)) } else { signer, err = ssh.ParsePrivateKey([]byte(key)) } if err != nil { return nil, fmt.Errorf("ssh: %w", err) } authMethods = append(authMethods, ssh.PublicKeys(signer)) } default: return nil, fmt.Errorf("ssh: unsupported auth method '%s'", authMethodType) } addr := resolveAddr(config.Host, config.Port) sshConn, chans, reqs, err := ssh.NewClientConn(conn, addr, &ssh.ClientConfig{ User: config.Username, Auth: authMethods, HostKeyCallback: ssh.InsecureIgnoreHostKey(), }) if err != nil { return nil, fmt.Errorf("ssh: %w", err) } return ssh.NewClient(sshConn, chans, reqs), nil } func resolveAddr(host string, port int) string { if port == 0 { port = defaultPort } return net.JoinHostPort(host, strconv.Itoa(port)) } ================================================ FILE: internal/tools/ssh/config.go ================================================ package ssh const ( defaultPort int = 22 defaultAuthMethod AuthMethodType = AuthMethodTypeNone defaultUsername string = "root" ) type ServerConfig struct { Host string Port int AuthMethod AuthMethodType Username string Password string Key string KeyPassphrase string } type Config struct { ServerConfig JumpServers []ServerConfig } func NewServerConfig() *ServerConfig { return &ServerConfig{ Port: defaultPort, AuthMethod: defaultAuthMethod, Username: defaultUsername, } } func NewDefaultConfig() *Config { return &Config{ ServerConfig: *NewServerConfig(), } } ================================================ FILE: internal/workflow/dispatcher/deps.go ================================================ package dispatcher import ( "context" "github.com/certimate-go/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) SaveWithCascading(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error) } type workflowLogRepository interface { Save(ctx context.Context, workflowLog *domain.WorkflowLog) (*domain.WorkflowLog, error) } ================================================ FILE: internal/workflow/dispatcher/dispatcher.go ================================================ package dispatcher import ( "context" "errors" "fmt" "log" "log/slog" "runtime" "runtime/debug" "sync" "time" "github.com/samber/lo" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/internal/repository" "github.com/certimate-go/certimate/internal/workflow/engine" "github.com/certimate-go/certimate/pkg/logging" xenv "github.com/certimate-go/certimate/pkg/utils/env" ) var envMaxWorkers = 1 func init() { envMaxWorkers = xenv.GetOrDefaultInt("CERTIMATE_WORKFLOW_MAX_WORKERS", runtime.GOMAXPROCS(0)) if envMaxWorkers <= 0 { envMaxWorkers = max(1, runtime.NumCPU()) } } type WorkflowDispatcher interface { GetStatistics() Statistics Bootup(ctx context.Context) error Shutdown(ctx context.Context) error Start(ctx context.Context, runId string) error Cancel(ctx context.Context, runId string) error } type Statistics struct { Concurrency int PendingRunIds []string ProcessingRunIds []string } type workflowDispatcher struct { booted bool concurrency int taskMtx sync.RWMutex pendingRunQueue []string processingTasks map[string]*taskInfo // Key: RunId workflowRepo workflowRepository workflowRunRepo workflowRunRepository workflowLogRepo workflowLogRepository syslog *slog.Logger } var _ WorkflowDispatcher = (*workflowDispatcher)(nil) func (wd *workflowDispatcher) GetStatistics() Statistics { wd.taskMtx.RLock() defer wd.taskMtx.RUnlock() stats := Statistics{ Concurrency: wd.concurrency, PendingRunIds: make([]string, 0), ProcessingRunIds: make([]string, 0), } for _, pendingRunId := range wd.pendingRunQueue { stats.PendingRunIds = append(stats.PendingRunIds, pendingRunId) } for _, processingRunId := range wd.processingTasks { stats.ProcessingRunIds = append(stats.ProcessingRunIds, processingRunId.RunId) } return stats } func (wd *workflowDispatcher) Bootup(ctx context.Context) error { if wd.booted { return errors.New("could not re-bootup") } wd.taskMtx.Lock() defer wd.taskMtx.Unlock() if _, err := app.GetDB().NewQuery(fmt.Sprintf("UPDATE %s SET lastRunStatus = 'canceled' WHERE lastRunStatus = 'pending' OR lastRunStatus = 'processing'", domain.CollectionNameWorkflow)).Execute(); err != nil { return err } if _, err := app.GetDB().NewQuery(fmt.Sprintf("UPDATE %s SET status = 'canceled' WHERE status = 'pending' OR status = 'processing'", domain.CollectionNameWorkflowRun)).Execute(); err != nil { return err } wd.booted = true return nil } func (wd *workflowDispatcher) Shutdown(ctx context.Context) error { if !wd.booted { return errors.New("could not re-shutdown") } wd.taskMtx.Lock() defer wd.taskMtx.Unlock() for runId, task := range wd.processingTasks { task.cancel() delete(wd.processingTasks, runId) } wd.booted = false wd.pendingRunQueue = make([]string, 0) wd.processingTasks = make(map[string]*taskInfo) return nil } func (wd *workflowDispatcher) Start(ctx context.Context, runId string) error { wd.taskMtx.Lock() defer wd.taskMtx.Unlock() if _, exists := wd.processingTasks[runId]; exists { return fmt.Errorf("workflow run %s is already processing", runId) } for _, pendingRunId := range wd.pendingRunQueue { if pendingRunId == runId { return fmt.Errorf("workflow run %s is already in the queue", runId) } } wd.pendingRunQueue = append(wd.pendingRunQueue, runId) go func() { wd.tryNextAsync() }() return nil } func (wd *workflowDispatcher) Cancel(ctx context.Context, runId string) error { wd.taskMtx.Lock() defer wd.taskMtx.Unlock() workflowRun, err := wd.workflowRunRepo.GetById(ctx, runId) if err != nil { return err } else if workflowRun.Status != domain.WorkflowRunStatusTypePending && workflowRun.Status != domain.WorkflowRunStatusTypeProcessing { return fmt.Errorf("workrun #%s is already completed", workflowRun.Id) } workflow, err := wd.workflowRepo.GetById(ctx, workflowRun.WorkflowId) if err != nil { return err } workflowRun.Status = domain.WorkflowRunStatusTypeCanceled if workflow.LastRunId == workflowRun.Id { _, err := wd.workflowRunRepo.SaveWithCascading(ctx, workflowRun) if err != nil { return err } } else { _, err := wd.workflowRunRepo.Save(ctx, workflowRun) if err != nil { return err } } if task, exists := wd.processingTasks[runId]; exists { task.cancel() delete(wd.processingTasks, runId) wd.syslog.Info(fmt.Sprintf("workrun #%s was canceled", task.RunId)) } for i, pendingRunId := range wd.pendingRunQueue { if pendingRunId == runId { wd.pendingRunQueue = append(wd.pendingRunQueue[:i], wd.pendingRunQueue[i+1:]...) break } } go func() { wd.tryNextAsync() }() return nil } func (wd *workflowDispatcher) tryExecuteAsync(task *taskInfo) { var workflow *domain.Workflow var workflowRun *domain.WorkflowRun var err error // 捕获 panic defer func() { if r := recover(); r != nil { wd.syslog.Error(fmt.Sprintf("workflow dispatcher panic: %v", r), slog.String("workflowId", task.WorkflowId), slog.String("runId", task.RunId)) slog.Error(fmt.Sprintf("workflow dispatcher panic: %v, stack trace: %s", r, string(debug.Stack())), slog.String("workflowId", task.WorkflowId), slog.String("runId", task.RunId)) if workflowRun != nil { workflowRun.Status = domain.WorkflowRunStatusTypeFailed workflowRun.EndedAt = time.Now() workflowRun.Error = fmt.Sprintf("workflow dispatcher panic: %v", r) if _, err := wd.workflowRunRepo.SaveWithCascading(context.Background(), workflowRun); err != nil { log.Default().Println("failed to save workflow run after panic", slog.Any("error", err)) } } } }() // 尝试继续执行等待队列中的任务 defer func() { wd.taskMtx.Lock() delete(wd.processingTasks, task.RunId) wd.taskMtx.Unlock() go func() { wd.tryNextAsync() }() }() // 查询运行实体,并级联更新状态 if workflowRun, err = wd.workflowRunRepo.GetById(task.ctx, task.RunId); err != nil { if !(errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) { wd.syslog.Error(fmt.Sprintf("failed to get workrun #%s record", task.RunId), slog.Any("error", err)) } return } else { if workflowRun.Status == domain.WorkflowRunStatusTypePending { workflowRun.Status = domain.WorkflowRunStatusTypeProcessing wd.workflowRunRepo.SaveWithCascading(task.ctx, workflowRun) } else { // WTF? That should be impossible! return } } // 查询工作流实体 workflow, err = wd.workflowRepo.GetById(task.ctx, workflowRun.WorkflowId) if err != nil { wd.syslog.Error(fmt.Sprintf("failed to get workflow #%s record", workflowRun.WorkflowId), slog.Any("error", err)) return } // 初始化工作流引擎 logsBuf := make(domain.WorkflowLogs, 0) we := engine.NewWorkflowEngine() we.OnEnd(func(ctx context.Context) error { if errmsg := logsBuf.ErrorString(); errmsg == "" { workflowRun.Status = domain.WorkflowRunStatusTypeSucceeded workflowRun.EndedAt = time.Now() } else { workflowRun.Status = domain.WorkflowRunStatusTypeFailed workflowRun.EndedAt = time.Now() workflowRun.Error = errmsg } wd.workflowRunRepo.SaveWithCascading(task.ctx, workflowRun) return nil }) we.OnError(func(ctx context.Context, err error) error { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { workflowRun.Status = domain.WorkflowRunStatusTypeCanceled wd.workflowRunRepo.SaveWithCascading(context.Background(), workflowRun) } else { workflowRun.Status = domain.WorkflowRunStatusTypeFailed workflowRun.EndedAt = time.Now() workflowRun.Error = err.Error() wd.workflowRunRepo.SaveWithCascading(task.ctx, workflowRun) } return nil }) we.OnNodeError(func(ctx context.Context, node *engine.Node, err error) error { if errors.Is(err, engine.ErrTerminated) || errors.Is(err, engine.ErrBlocksException) { return nil } log := domain.WorkflowLog{} log.WorkflowId = task.WorkflowId log.RunId = task.RunId log.NodeId = node.Id log.NodeName = node.Data.Name log.TimestampMilli = time.Now().UnixMilli() log.Level = int32(slog.LevelError) log.Message = err.Error() log.CreatedAt = time.Now() logsBuf = append(logsBuf, log) if _, err := wd.workflowLogRepo.Save(ctx, &log); err != nil { wd.syslog.Error(err.Error()) } return nil }) we.OnNodeLogging(func(ctx context.Context, node *engine.Node, record logging.Record) error { log := domain.WorkflowLog{} log.WorkflowId = task.WorkflowId log.RunId = task.RunId log.NodeId = node.Id log.NodeName = node.Data.Name log.TimestampMilli = record.Time.UnixMilli() log.Level = int32(record.Level) log.Message = record.Message log.Data = record.Data() log.CreatedAt = time.Now() logsBuf = append(logsBuf, log) if _, err := wd.workflowLogRepo.Save(ctx, &log); err != nil { wd.syslog.Error(err.Error()) } return nil }) // 执行工作流 wd.syslog.Info(fmt.Sprintf("workflow #%s's run #%s started", task.WorkflowId, task.RunId)) we.Invoke(task.ctx, engine.WorkflowExecution{ WorkflowId: workflowRun.WorkflowId, WorkflowName: workflow.Name, RunId: workflowRun.Id, RunTrigger: workflowRun.Trigger, Graph: workflowRun.Graph, }) wd.syslog.Info(fmt.Sprintf("workflow #%s's run #%s stopped", task.WorkflowId, task.RunId)) } func (wd *workflowDispatcher) tryNextAsync() { wd.taskMtx.RLock() for _, pendingRunId := range wd.pendingRunQueue { workflowRun, err := wd.workflowRunRepo.GetById(context.Background(), pendingRunId) if err != nil { wd.syslog.Error(fmt.Sprintf("failed to get workrun #%s record", pendingRunId), slog.Any("error", err)) continue } var hasSameWorkflowTask bool // 相同 Workflow 的任务同一时间只能有一个 Run 在执行 for _, processingTask := range wd.processingTasks { if processingTask.WorkflowId == workflowRun.WorkflowId { hasSameWorkflowTask = true break } } if hasSameWorkflowTask { wd.syslog.Warn(fmt.Sprintf("workflow #%s's run #%s is pending, because tasks that belonging to the same workflow already exists", workflowRun.WorkflowId, workflowRun.Id)) } else if len(wd.processingTasks) >= wd.concurrency && wd.concurrency > 0 { wd.syslog.Warn(fmt.Sprintf("workflow #%s's run #%s is pending, because the maximum concurrency (limit: %d) has been reached", workflowRun.WorkflowId, workflowRun.Id, wd.concurrency)) } else { wd.taskMtx.RUnlock() wd.taskMtx.Lock() ctxRun, ctxCancel := context.WithCancel(context.Background()) task := &taskInfo{WorkflowId: workflowRun.WorkflowId, RunId: workflowRun.Id, ctx: ctxRun, cancel: ctxCancel} wd.pendingRunQueue = lo.Filter(wd.pendingRunQueue, func(s string, _ int) bool { return s != pendingRunId }) wd.processingTasks[pendingRunId] = task wd.syslog.Info(fmt.Sprintf("workflow #%s's run #%s is being dispatched ...", task.WorkflowId, task.RunId)) wd.taskMtx.Unlock() go func() { wd.tryExecuteAsync(task) }() return } } wd.taskMtx.RUnlock() } func newWorkflowDispatcher() WorkflowDispatcher { return &workflowDispatcher{ concurrency: envMaxWorkers, pendingRunQueue: make([]string, 0), processingTasks: make(map[string]*taskInfo), workflowRepo: repository.NewWorkflowRepository(), workflowRunRepo: repository.NewWorkflowRunRepository(), workflowLogRepo: repository.NewWorkflowLogRepository(), syslog: app.GetLogger(), } } ================================================ FILE: internal/workflow/dispatcher/singleton.go ================================================ package dispatcher import ( "sync" ) var ( instance WorkflowDispatcher intanceOnce sync.Once ) func GetSingletonDispatcher() WorkflowDispatcher { intanceOnce.Do(func() { instance = newWorkflowDispatcher() }) return instance } ================================================ FILE: internal/workflow/dispatcher/task.go ================================================ package dispatcher import ( "context" ) type taskInfo struct { WorkflowId string RunId string ctx context.Context cancel context.CancelFunc } ================================================ FILE: internal/workflow/engine/context.go ================================================ package engine import ( "context" ) type WorkflowContext struct { WorkflowId string RunId string RunGraph *Graph engine WorkflowEngine variables VariableManager inputs InOutManager ctx context.Context } func (c *WorkflowContext) SetExecutingWorkflow(workflowId string, runId string, runGraph *Graph) *WorkflowContext { c.WorkflowId = workflowId c.RunId = runId c.RunGraph = runGraph return c } func (c *WorkflowContext) SetEngine(engine WorkflowEngine) *WorkflowContext { c.engine = engine return c } func (c *WorkflowContext) SetVariablesManager(inputs VariableManager) *WorkflowContext { c.variables = inputs return c } func (c *WorkflowContext) SetInputsManager(manager InOutManager) *WorkflowContext { c.inputs = manager return c } func (c *WorkflowContext) SetContext(ctx context.Context) *WorkflowContext { c.ctx = ctx return c } func (c *WorkflowContext) Context() context.Context { return c.ctx } func (c *WorkflowContext) Clone() *WorkflowContext { return &WorkflowContext{ WorkflowId: c.WorkflowId, RunId: c.RunId, RunGraph: c.RunGraph, engine: c.engine, variables: c.variables, inputs: c.inputs, ctx: c.ctx, } } ================================================ FILE: internal/workflow/engine/deps.go ================================================ package engine import ( "context" "github.com/certimate-go/certimate/internal/domain" ) type accessRepository interface { GetById(ctx context.Context, id string) (*domain.Access, error) } type certificateRepository interface { GetById(ctx context.Context, id string) (*domain.Certificate, error) GetByWorkflowRunIdAndNodeId(ctx context.Context, workflowRunId string, workflowNodeId string) (*domain.Certificate, error) Save(ctx context.Context, certificate *domain.Certificate) (*domain.Certificate, error) } type workflowOutputRepository interface { GetByWorkflowIdAndNodeId(ctx context.Context, workflowId string, workflowNodeId string) (*domain.WorkflowOutput, error) Save(ctx context.Context, workflowOutput *domain.WorkflowOutput) (*domain.WorkflowOutput, error) } type settingsRepository interface { GetByName(ctx context.Context, name string) (*domain.Settings, error) } ================================================ FILE: internal/workflow/engine/engine.go ================================================ package engine import ( "context" "errors" "fmt" "log/slog" "runtime/debug" "sync" "github.com/samber/lo" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/internal/repository" "github.com/certimate-go/certimate/pkg/logging" ) type WorkflowExecution struct { WorkflowId string WorkflowName string RunId string RunTrigger domain.WorkflowTriggerType Graph *Graph } type WorkflowEngine interface { Invoke(ctx context.Context, execution WorkflowExecution) error OnStart(callback func(ctx context.Context) error) OnEnd(callback func(ctx context.Context) error) OnError(callback func(ctx context.Context, err error) error) OnNodeStart(callback func(ctx context.Context, node *Node) error) OnNodeEnd(callback func(ctx context.Context, node *Node, res *NodeExecutionResult) error) OnNodeError(callback func(ctx context.Context, node *Node, err error) error) OnNodeLogging(callback func(ctx context.Context, node *Node, log logging.Record) error) } type workflowEngine struct { executors map[NodeType]NodeExecutor hooksMtx sync.RWMutex onStartHooks [](func(ctx context.Context) error) onEndHooks [](func(ctx context.Context) error) onErrorHooks [](func(ctx context.Context, err error) error) onNodeStartHooks [](func(ctx context.Context, node *Node) error) onNodeEndHooks [](func(ctx context.Context, node *Node, res *NodeExecutionResult) error) onNodeErrorHooks [](func(ctx context.Context, node *Node, err error) error) onNodeLoggingHooks [](func(ctx context.Context, node *Node, log logging.Record) error) wfoutputRepo workflowOutputRepository syslog *slog.Logger } var _ WorkflowEngine = (*workflowEngine)(nil) func (we *workflowEngine) Invoke(ctx context.Context, execution WorkflowExecution) error { defer func() { if r := recover(); r != nil { we.fireOnErrorHooks(ctx, fmt.Errorf("workflow engine panic: %v", r)) we.syslog.Error(fmt.Sprintf("workflow engine panic: %v", r), slog.String("workflowId", execution.WorkflowId), slog.String("runId", execution.RunId)) slog.Error(fmt.Sprintf("workflow engine panic: %v, stack trace: %s", r, string(debug.Stack())), slog.String("workflowId", execution.WorkflowId), slog.String("runId", execution.RunId)) } }() we.fireOnStartHooks(ctx) wfIOs := newInOutManager() wfVars := newVariableManager() wfVars.Set(stateVarKeyWorkflowId, execution.WorkflowId, stateValTypeString) wfVars.Set(stateVarKeyWorkflowName, execution.WorkflowName, stateValTypeString) wfVars.Set(stateVarKeyRunId, execution.RunId, stateValTypeString) wfVars.Set(stateVarKeyRunTrigger, execution.RunTrigger, stateValTypeString) wfVars.Set(stateVarKeyErrorNodeId, "", stateValTypeString) wfVars.Set(stateVarKeyErrorNodeName, "", stateValTypeString) wfVars.Set(stateVarKeyErrorMessage, "", stateValTypeString) wfCtx := (&WorkflowContext{}). SetExecutingWorkflow(execution.WorkflowId, execution.RunId, execution.Graph). SetEngine(we). SetInputsManager(wfIOs). SetVariablesManager(wfVars). SetContext(ctx) if err := we.executeBlocks(wfCtx, execution.Graph.Nodes); err != nil { if !errors.Is(err, ErrTerminated) { we.fireOnErrorHooks(ctx, err) return err } } we.fireOnEndHooks(ctx) return nil } func (we *workflowEngine) OnStart(callback func(ctx context.Context) error) { we.hooksMtx.Lock() defer we.hooksMtx.Unlock() we.onStartHooks = append(we.onStartHooks, callback) } func (we *workflowEngine) OnEnd(callback func(ctx context.Context) error) { we.hooksMtx.Lock() defer we.hooksMtx.Unlock() we.onEndHooks = append(we.onEndHooks, callback) } func (we *workflowEngine) OnError(callback func(ctx context.Context, err error) error) { we.hooksMtx.Lock() defer we.hooksMtx.Unlock() we.onErrorHooks = append(we.onErrorHooks, callback) } func (we *workflowEngine) OnNodeStart(callback func(ctx context.Context, node *Node) error) { we.hooksMtx.Lock() defer we.hooksMtx.Unlock() we.onNodeStartHooks = append(we.onNodeStartHooks, callback) } func (we *workflowEngine) OnNodeEnd(callback func(ctx context.Context, node *Node, res *NodeExecutionResult) error) { we.hooksMtx.Lock() defer we.hooksMtx.Unlock() we.onNodeEndHooks = append(we.onNodeEndHooks, callback) } func (we *workflowEngine) OnNodeError(callback func(ctx context.Context, node *Node, err error) error) { we.hooksMtx.Lock() defer we.hooksMtx.Unlock() we.onNodeErrorHooks = append(we.onNodeErrorHooks, callback) } func (we *workflowEngine) OnNodeLogging(callback func(ctx context.Context, node *Node, log logging.Record) error) { we.hooksMtx.Lock() defer we.hooksMtx.Unlock() we.onNodeLoggingHooks = append(we.onNodeLoggingHooks, callback) } func (we *workflowEngine) executeNode(wfCtx *WorkflowContext, node *Node) error { executor, ok := we.executors[node.Type] if !ok { err := fmt.Errorf("workflow engine: no executor registered for node type: '%s'", node.Type) return err } else { logger := slog.New(logging.NewHookHandler(&logging.HookHandlerOptions{ Level: slog.LevelDebug, WriteFunc: func(ctx context.Context, record logging.Record) error { we.fireOnNodeLoggingHooks(ctx, node, record) return nil }, })) executor.SetLogger(logger) } wfCtx.variables.SetScoped(node.Id, stateVarKeyNodeId, node.Id, stateValTypeString) wfCtx.variables.SetScoped(node.Id, stateVarKeyNodeName, node.Data.Name, stateValTypeString) // 节点已禁用,直接跳过执行 if node.Data.Disabled { return nil } we.fireOnNodeStartHooks(wfCtx.ctx, node) execCtx := newNodeExecutionContext(wfCtx, node) execRes, err := executor.Execute(execCtx) if err != nil && !errors.Is(err, ErrTerminated) { if !errors.Is(err, ErrBlocksException) { wfCtx.variables.Set(stateVarKeyErrorNodeId, node.Id, stateValTypeString) wfCtx.variables.Set(stateVarKeyErrorNodeName, node.Data.Name, stateValTypeString) wfCtx.variables.Set(stateVarKeyErrorMessage, err.Error(), stateValTypeString) } we.fireOnNodeErrorHooks(wfCtx.ctx, node, err) return err } we.fireOnNodeEndHooks(wfCtx.ctx, node, execRes) if execRes != nil { if execRes.Variables != nil { for _, variable := range execRes.Variables { wfCtx.variables.Add(variable) } } if execRes.Outputs != nil { for _, output := range execRes.Outputs { wfCtx.inputs.Add(output) } } execOutputs := lo.Filter(execRes.Outputs, func(state InOutState, _ int) bool { return state.Persistent }) if execRes.outputForced || len(execOutputs) > 0 { output := &domain.WorkflowOutput{ WorkflowId: execCtx.WorkflowId, RunId: execCtx.RunId, NodeId: execCtx.Node.Id, NodeConfig: execCtx.Node.Data.Config, Succeeded: true, // TODO: 目前恒为 true } if len(execOutputs) > 0 { output.Outputs = lo.Map(execOutputs, func(state InOutState, _ int) *domain.WorkflowOutputEntry { return &domain.WorkflowOutputEntry{ Name: state.Name, Type: state.Type, Value: state.ValueString(), ValueType: state.ValueType, } }) } if _, err := we.wfoutputRepo.Save(execCtx.Context(), output); err != nil { we.syslog.Error("failed to save node output", slog.Any("error", err)) } } if execRes.Terminated { return ErrTerminated } } if err != nil && errors.Is(err, ErrTerminated) { return err } return nil } func (we *workflowEngine) executeBlocks(wfCtx *WorkflowContext, blocks []*Node) error { errs := make([]error, 0) for _, node := range blocks { select { case <-wfCtx.ctx.Done(): return wfCtx.ctx.Err() default: } err := we.executeNode(wfCtx, node) if err != nil { // 如果当前节点是 TryCatch 节点、且在 CatchBlock 分支中没有 End 节点, // 则暂存错误,但继续执行下一个节点,直到当前 Blocks 全部执行完毕。 if node.Type == NodeTypeTryCatch { if !errors.Is(err, ErrTerminated) { errs = append(errs, err) continue } } return err } } if len(errs) > 0 { if len(errs) == 1 { return errs[0] } return errors.Join(errs...) } return nil } func (we *workflowEngine) fireOnStartHooks(ctx context.Context) { we.hooksMtx.RLock() defer we.hooksMtx.RUnlock() for _, cb := range we.onStartHooks { if cbErr := cb(ctx); cbErr != nil { we.syslog.Error("workflow engine: error in onStart hook", slog.Any("error", cbErr)) } } } func (we *workflowEngine) fireOnEndHooks(ctx context.Context) { we.hooksMtx.RLock() defer we.hooksMtx.RUnlock() for _, cb := range we.onEndHooks { if cbErr := cb(ctx); cbErr != nil { we.syslog.Error("workflow engine: error in onEnd hook", slog.Any("error", cbErr)) } } } func (we *workflowEngine) fireOnErrorHooks(ctx context.Context, err error) { we.hooksMtx.RLock() defer we.hooksMtx.RUnlock() for _, cb := range we.onErrorHooks { if cbErr := cb(ctx, err); cbErr != nil { we.syslog.Error("workflow engine: error in onError hook", slog.Any("error", cbErr)) } } } func (we *workflowEngine) fireOnNodeStartHooks(ctx context.Context, node *Node) { we.hooksMtx.RLock() defer we.hooksMtx.RUnlock() for _, cb := range we.onNodeStartHooks { if cbErr := cb(ctx, node); cbErr != nil { we.syslog.Error("workflow engine: error in onNodeStart hook", slog.Any("error", cbErr)) } } } func (we *workflowEngine) fireOnNodeEndHooks(ctx context.Context, node *Node, result *NodeExecutionResult) { we.hooksMtx.RLock() defer we.hooksMtx.RUnlock() for _, cb := range we.onNodeEndHooks { if cbErr := cb(ctx, node, result); cbErr != nil { we.syslog.Error("workflow engine: error in onNodeEnd hook", slog.Any("error", cbErr)) } } } func (we *workflowEngine) fireOnNodeErrorHooks(ctx context.Context, node *Node, err error) { we.hooksMtx.RLock() defer we.hooksMtx.RUnlock() for _, cb := range we.onNodeErrorHooks { if cbErr := cb(ctx, node, err); cbErr != nil { we.syslog.Error("workflow engine: error in onNodeError hook", slog.Any("error", cbErr)) } } } func (we *workflowEngine) fireOnNodeLoggingHooks(ctx context.Context, node *Node, log logging.Record) { we.hooksMtx.RLock() defer we.hooksMtx.RUnlock() for _, cb := range we.onNodeLoggingHooks { if cbErr := cb(ctx, node, log); cbErr != nil { we.syslog.Error("workflow engine: error in onNodeLogging hook", slog.Any("error", cbErr)) } } } func NewWorkflowEngine() WorkflowEngine { engine := &workflowEngine{ executors: make(map[NodeType]NodeExecutor), wfoutputRepo: repository.NewWorkflowOutputRepository(), syslog: app.GetLogger(), } engine.executors[NodeTypeStart] = newStartNodeExecutor() engine.executors[NodeTypeEnd] = newEndNodeExecutor() engine.executors[NodeTypeDelay] = newDelayNodeExecutor() engine.executors[NodeTypeCondition] = newConditionNodeExecutor() engine.executors[NodeTypeBranchBlock] = newBranchBlockNodeExecutor() engine.executors[NodeTypeTryCatch] = newTryCatchNodeExecutor() engine.executors[NodeTypeTryBlock] = newTryBlockNodeExecutor() engine.executors[NodeTypeCatchBlock] = newCatchBlockNodeExecutor() engine.executors[NodeTypeBizApply] = newBizApplyNodeExecutor() engine.executors[NodeTypeBizUpload] = newBizUploadNodeExecutor() engine.executors[NodeTypeBizMonitor] = newBizMonitorNodeExecutor() engine.executors[NodeTypeBizDeploy] = newBizDeployNodeExecutor() engine.executors[NodeTypeBizNotify] = newBizNotifyNodeExecutor() return engine } ================================================ FILE: internal/workflow/engine/errors.go ================================================ package engine import ( "errors" ) var ( // 表示工作流引擎执行被中断,可能已结束 ErrTerminated = errors.New("workflow engine: execution was terminated") // 表示工作流引擎在执行子节点时发生异常 ErrBlocksException = errors.New("workflow engine: error occurred when executing blocks") ) ================================================ FILE: internal/workflow/engine/executor.go ================================================ package engine import ( "context" "log/slog" "sync" ) type NodeExecutor interface { withLogger Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) } type nodeExecutor struct { logger *slog.Logger } func (e *nodeExecutor) SetLogger(logger *slog.Logger) { e.logger = logger } type NodeExecutionContext struct { WorkflowContext Node *Node } func (c *NodeExecutionContext) SetExecutingWorkflow(workflowId string, runId string, runGraph *Graph) *NodeExecutionContext { c.WorkflowContext.SetExecutingWorkflow(workflowId, runId, runGraph) return c } func (c *NodeExecutionContext) SetExecutingNode(node *Node) *NodeExecutionContext { c.Node = node return c } func (c *NodeExecutionContext) SetEngine(engine WorkflowEngine) *NodeExecutionContext { c.WorkflowContext.SetEngine(engine) return c } func (c *NodeExecutionContext) SetVariablesManager(variables VariableManager) *NodeExecutionContext { c.WorkflowContext.SetVariablesManager(variables) return c } func (c *NodeExecutionContext) SetInputsManager(inputs InOutManager) *NodeExecutionContext { c.WorkflowContext.SetInputsManager(inputs) return c } func (c *NodeExecutionContext) SetContext(ctx context.Context) *NodeExecutionContext { c.WorkflowContext.SetContext(ctx) return c } func newNodeExecutionContext(wfCtx *WorkflowContext, node *Node) *NodeExecutionContext { return (&NodeExecutionContext{}). SetExecutingWorkflow(wfCtx.WorkflowId, wfCtx.RunId, wfCtx.RunGraph). SetExecutingNode(node). SetEngine(wfCtx.engine). SetVariablesManager(wfCtx.variables). SetInputsManager(wfCtx.inputs). SetContext(wfCtx.ctx) } type NodeExecutionResult struct { node *Node Terminated bool // 是否终止执行(通常由 End 节点主动触发) variablesMtx sync.Mutex Variables []VariableState outputForced bool // 即使 Outputs 为空,也强制持久化输出 outputsMtx sync.Mutex Outputs []InOutState } func (r *NodeExecutionResult) AddVariable(key string, value any, valueType string) { r.AddVariableWithScope("", key, value, valueType) } func (r *NodeExecutionResult) AddVariableWithScope(scope string, key string, value any, valueType string) { r.addVariableState(VariableState{ Scope: scope, Key: key, Value: value, ValueType: valueType, }) } func (r *NodeExecutionResult) addVariableState(state VariableState) { r.variablesMtx.Lock() defer r.variablesMtx.Unlock() if r.Variables == nil { r.Variables = make([]VariableState, 0) } for i, item := range r.Variables { if item.Scope == state.Scope && item.Key == state.Key { r.Variables[i] = state return } } r.Variables = append(r.Variables, state) } func (r *NodeExecutionResult) AddOutput(stype string, key string, value any, valueType string) { r.addOutputState(InOutState{ NodeId: r.node.Id, Type: stype, Name: key, Value: value, ValueType: valueType, Persistent: false, }) } func (r *NodeExecutionResult) AddOutputWithPersistent(stype string, key string, value any, valueType string) { r.addOutputState(InOutState{ NodeId: r.node.Id, Type: stype, Name: key, Value: value, ValueType: valueType, Persistent: true, }) } func (r *NodeExecutionResult) addOutputState(state InOutState) { r.outputsMtx.Lock() defer r.outputsMtx.Unlock() if r.Outputs == nil { r.Outputs = make([]InOutState, 0) } for i, t := range r.Outputs { if t.NodeId == state.NodeId && t.Name == state.Name { r.Outputs[i] = state return } } r.Outputs = append(r.Outputs, state) } func newNodeExecutionResult(node *Node) *NodeExecutionResult { return &NodeExecutionResult{ node: node, Variables: make([]VariableState, 0), Outputs: make([]InOutState, 0), } } ================================================ FILE: internal/workflow/engine/executor_bizapply.go ================================================ package engine import ( "crypto/x509" "fmt" "log/slog" "maps" "math" "slices" "strings" "time" legocertifier "github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/lego" legolog "github.com/go-acme/lego/v4/log" "github.com/samber/lo" "github.com/xhit/go-str2duration/v2" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/internal/certacme" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/internal/repository" "github.com/certimate-go/certimate/internal/tools/mproc" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcertkey "github.com/certimate-go/certimate/pkg/utils/cert/key" xenv "github.com/certimate-go/certimate/pkg/utils/env" ) var envMultiProc = true func init() { envMultiProc = xenv.GetOrDefaultBool("CERTIMATE_WORKFLOW_MULTIPROC", true) } const ( BizApplyKeySourceAuto = "auto" BizApplyKeySourceReuse = "reuse" BizApplyKeySourceCustom = "custom" ) /** * Outputs: * - ref: "certificate": string * * Variables: * - "node.skipped": boolean * - "certificate.commanName": string * - "certificate.subjectAltNames": string * - "certificate.notBefore": datetime * - "certificate.notAfter": datetime * - "certificate.hoursLeft": number * - "certificate.daysLeft": number * - "certificate.validity": boolean */ type bizApplyNodeExecutor struct { nodeExecutor accessRepo accessRepository certificateRepo certificateRepository wfoutputRepo workflowOutputRepository } func (ne *bizApplyNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { execRes := newNodeExecutionResult(execCtx.Node) nodeCfg := execCtx.Node.Data.Config.AsBizApply() ne.logger.Info("ready to request certificate ...", slog.Any("config", nodeCfg)) // 查询上次执行结果 lastOutput, lastCertificate, err := ne.getLastOutputArtifacts(execCtx) if err != nil { return execRes, err } else { if lastOutput != nil { ne.logger.Info(fmt.Sprintf("found last node output #%s record", lastOutput.RunId)) } if lastCertificate != nil { ne.setOuputsOfResult(execCtx, execRes, lastCertificate, false) ne.setVariablesOfResult(execCtx, execRes, lastCertificate) ne.logger.Info(fmt.Sprintf("found last certificate #%s record", lastCertificate.Id)) } } // 检测是否可以跳过本次执行 if skippable, reason := ne.checkCanSkip(execCtx, lastOutput, lastCertificate); skippable { ne.logger.Info(fmt.Sprintf("skip this application, because %s", reason)) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyNodeSkipped, true, stateValTypeBoolean) return execRes, nil } else { if reason != "" { ne.logger.Info(fmt.Sprintf("re-apply, because %s", reason)) } else { ne.logger.Info("no found last requested certificate, begin to apply") } execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyNodeSkipped, false, stateValTypeBoolean) } // 申请证书 obtainResp, err := ne.executeObtain(execCtx, &nodeCfg, lastCertificate) if err != nil { return execRes, err } // 保存证书实体 certificate := &domain.Certificate{ Source: domain.CertificateSourceTypeRequest, Certificate: obtainResp.FullChainCertificate, PrivateKey: obtainResp.PrivateKey, IssuerCertificate: obtainResp.IssuerCertificate, ACMEAcctUrl: obtainResp.ACMEAcctUrl, ACMECertUrl: obtainResp.ACMECertUrl, WorkflowId: execCtx.WorkflowId, WorkflowRunId: execCtx.RunId, WorkflowNodeId: execCtx.Node.Id, } certificate.PopulateFromPEM(obtainResp.FullChainCertificate, obtainResp.PrivateKey) if certificate, err := ne.certificateRepo.Save(execCtx.Context(), certificate); err != nil { ne.logger.Warn("could not save certificate") return execRes, err } else { ne.logger.Info("certificate saved", slog.String("recordId", certificate.Id)) } // 保存 ARI 替换状态 if lastCertificate != nil && obtainResp.ARIReplaced { lastCertificate.IsRenewed = true ne.certificateRepo.Save(execCtx.Context(), lastCertificate) } // 节点输出 ne.setOuputsOfResult(execCtx, execRes, certificate, true) ne.setVariablesOfResult(execCtx, execRes, certificate) ne.logger.Info("application completed") return execRes, nil } func (ne *bizApplyNodeExecutor) getLastOutputArtifacts(execCtx *NodeExecutionContext) (*domain.WorkflowOutput, *domain.Certificate, error) { lastOutput, err := ne.wfoutputRepo.GetByWorkflowIdAndNodeId(execCtx.Context(), execCtx.WorkflowId, execCtx.Node.Id) if err != nil && !domain.IsRecordNotFoundError(err) { return nil, nil, fmt.Errorf("failed to get last output record of node #%s: %w", execCtx.Node.Id, err) } if lastOutput != nil { lastCertificate, err := ne.certificateRepo.GetByWorkflowRunIdAndNodeId(execCtx.Context(), lastOutput.RunId, lastOutput.NodeId) if err != nil && !domain.IsRecordNotFoundError(err) { return lastOutput, nil, fmt.Errorf("failed to get last certificate record of node #%s: %w", execCtx.Node.Id, err) } return lastOutput, lastCertificate, nil } return lastOutput, nil, nil } func (ne *bizApplyNodeExecutor) checkCanSkip(execCtx *NodeExecutionContext, lastOutput *domain.WorkflowOutput, lastCertificate *domain.Certificate) (_skip bool, _reason string) { thisNodeCfg := execCtx.Node.Data.Config.AsBizApply() if lastOutput != nil && lastOutput.Succeeded { // 比较和上次申请时的关键配置(即影响证书签发的)参数是否一致 lastNodeCfg := lastOutput.NodeConfig.AsBizApply() if !slices.Equal(thisNodeCfg.Domains, lastNodeCfg.Domains) { return false, "the configuration item 'Domains' changed" } if !slices.Equal(thisNodeCfg.IPAddrs, lastNodeCfg.IPAddrs) { return false, "the configuration item 'IPAddrs' changed" } if thisNodeCfg.ContactEmail != lastNodeCfg.ContactEmail { return false, "the configuration item 'ContactEmail' changed" } if thisNodeCfg.Provider != lastNodeCfg.Provider { return false, "the configuration item 'Provider' changed" } if thisNodeCfg.ProviderAccessId != lastNodeCfg.ProviderAccessId { return false, "the configuration item 'ProviderAccessId' changed" } if !maps.Equal(thisNodeCfg.ProviderConfig, lastNodeCfg.ProviderConfig) { return false, "the configuration item 'ProviderConfig' changed" } if thisNodeCfg.CAProvider != lastNodeCfg.CAProvider { return false, "the configuration item 'CAProvider' changed" } if thisNodeCfg.CAProviderAccessId != lastNodeCfg.CAProviderAccessId { return false, "the configuration item 'CAProviderAccessId' changed" } if !maps.Equal(thisNodeCfg.CAProviderConfig, lastNodeCfg.CAProviderConfig) { return false, "the configuration item 'CAProviderConfig' changed" } if thisNodeCfg.KeyAlgorithm != lastNodeCfg.KeyAlgorithm { return false, "the configuration item 'KeyAlgorithm' changed" } if thisNodeCfg.KeySource == BizApplyKeySourceCustom && thisNodeCfg.KeyContent != lastNodeCfg.KeyContent { return false, "the configuration item 'KeyContent' changed" } if thisNodeCfg.ValidityLifetime != lastNodeCfg.ValidityLifetime { return false, "the configuration item 'ValidityLifetime' changed" } if thisNodeCfg.PreferredChain != lastNodeCfg.PreferredChain { return false, "the configuration item 'PreferredChain' changed" } if thisNodeCfg.ACMEProfile != lastNodeCfg.ACMEProfile { return false, "the configuration item 'ACMEProfile' changed" } if thisNodeCfg.DisableCommonName != lastNodeCfg.DisableCommonName { return false, "the configuration item 'DisableCommonName' changed" } } if lastCertificate != nil { if lastCertificate.IsRevoked { return false, "the last requested certificate has been revoked" } renewalInterval := time.Duration(thisNodeCfg.SkipBeforeExpiryDays) * time.Hour * 24 expirationTime := time.Until(lastCertificate.ValidityNotAfter) daysLeft := int(math.Floor(expirationTime.Hours() / 24)) if expirationTime > renewalInterval { return true, fmt.Sprintf("the last requested certificate expires in %d day(s), next renewal will be in %d day(s)", daysLeft, thisNodeCfg.SkipBeforeExpiryDays) } return false, fmt.Sprintf("the last requested certificate expires in %d day(s), the renewal window period has been reached", daysLeft) } return false, "" } func (ne *bizApplyNodeExecutor) executeObtain(execCtx *NodeExecutionContext, nodeCfg *domain.WorkflowNodeConfigForBizApply, lastCertificate *domain.Certificate) (*certacme.ObtainCertificateResponse, error) { // 读取私钥算法 // 如果复用私钥,则保持算法一致 legoKeyType, err := domain.CertificateKeyAlgorithmType(nodeCfg.KeyAlgorithm).KeyType() if err != nil { return nil, err } else { switch nodeCfg.KeySource { case BizApplyKeySourceAuto: break case BizApplyKeySourceReuse: if lastCertificate != nil { legoKeyType, _ = lastCertificate.KeyAlgorithm.KeyType() } case BizApplyKeySourceCustom: privkey, err := xcert.ParsePrivateKeyFromPEM(nodeCfg.KeyContent) if err != nil { return nil, fmt.Errorf("could not parse custom private key: %w", err) } else { privkeyAlg, privkeySize, _ := xcertkey.GetPrivateKeyAlgorithm(privkey) switch privkeyAlg { case x509.RSA: if nodeCfg.KeyAlgorithm != fmt.Sprintf("RSA%d", privkeySize) { return nil, fmt.Errorf("could not parse custom private key: unsupported algorithm or key size") } case x509.ECDSA: if nodeCfg.KeyAlgorithm != fmt.Sprintf("EC%d", privkeySize) { return nil, fmt.Errorf("could not parse custom private key: unsupported algorithm or key size") } default: return nil, fmt.Errorf("could not parse custom private key: unsupported algorithm") } } } } // 读取质询提供商授权 providerAccessConfig := make(map[string]any) if nodeCfg.ProviderAccessId != "" { if access, err := ne.accessRepo.GetById(execCtx.Context(), nodeCfg.ProviderAccessId); err != nil { return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.ProviderAccessId, err) } else { providerAccessConfig = access.Config } } // 读取证书颁发机构授权 caAccessConfig := make(map[string]any) if nodeCfg.CAProviderAccessId != "" { if access, err := ne.accessRepo.GetById(execCtx.Context(), nodeCfg.CAProviderAccessId); err != nil { return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.CAProviderAccessId, err) } else { caAccessConfig = access.Config } } // 初始化 ACME 配置项 legoOptions := &certacme.ACMEConfigOptions{ CAProvider: nodeCfg.CAProvider, CAAccessConfig: caAccessConfig, CAProviderConfig: nodeCfg.CAProviderConfig, CertifierKeyType: legoKeyType, } legoConfig, err := certacme.NewACMEConfig(legoOptions) if err != nil { ne.logger.Warn("could not initialize acme config") return nil, err } else { ne.logger.Info("acme config initialized", slog.String("acmeDirUrl", legoConfig.CADirUrl)) } // 初始化 ACME 账户 // 注意此步骤仍需在主进程中进行,以保证并发安全 legoUser, err := certacme.NewACMEAccountWithSingleFlight(legoConfig, nodeCfg.ContactEmail) if err != nil { ne.logger.Warn("could not initialize acme account") return nil, err } else { ne.logger.Info("acme account initialized", slog.String("acmeAcctUrl", legoUser.ACMEAcctUrl)) } // 构造证书申请请求 legoDomains := make([]string, 0) legoDomains = append(legoDomains, nodeCfg.Domains...) legoDomains = append(legoDomains, nodeCfg.IPAddrs...) obtainReq := &certacme.ObtainCertificateRequest{ DomainOrIPs: legoDomains, PrivateKeyType: legoKeyType, PrivateKeyPEM: lo. If(nodeCfg.KeySource == BizApplyKeySourceAuto, ""). ElseF(func() string { switch nodeCfg.KeySource { case BizApplyKeySourceReuse: if lastCertificate != nil { return lastCertificate.PrivateKey } case BizApplyKeySourceCustom: return nodeCfg.KeyContent } return "" }), ValidityNotAfter: lo. If(nodeCfg.ValidityLifetime == "", time.Time{}). ElseF(func() time.Time { duration, err := str2duration.ParseDuration(nodeCfg.ValidityLifetime) if err != nil { return time.Time{} } return time.Now().Add(duration) }), NoCommonName: nodeCfg.DisableCommonName, ChallengeType: nodeCfg.ChallengeType, Provider: nodeCfg.Provider, ProviderAccessConfig: providerAccessConfig, ProviderExtendedConfig: nodeCfg.ProviderConfig, DisableFollowCNAME: nodeCfg.DisableFollowCNAME, Nameservers: nodeCfg.Nameservers, DnsPropagationWait: nodeCfg.DnsPropagationWait, DnsPropagationTimeout: nodeCfg.DnsPropagationTimeout, DnsTTL: nodeCfg.DnsTTL, HttpDelayWait: nodeCfg.HttpDelayWait, PreferredChain: nodeCfg.PreferredChain, ACMEProfile: nodeCfg.ACMEProfile, ARIReplacesAcctUrl: lo. If(nodeCfg.DisableARI || lastCertificate == nil, ""). ElseF(func() string { if lastCertificate.IsRenewed { return "" } return lastCertificate.ACMEAcctUrl }), ARIReplacesCertId: lo. If(nodeCfg.DisableARI || lastCertificate == nil, ""). ElseF(func() string { if lastCertificate.IsRenewed { return "" } newCertSan := slices.Clone(nodeCfg.Domains) oldCertSan := strings.Split(lastCertificate.SubjectAltNames, ";") slices.Sort(newCertSan) slices.Sort(oldCertSan) if !slices.Equal(newCertSan, oldCertSan) { return "" } oldCertX509, err := xcert.ParseCertificateFromPEM(lastCertificate.Certificate) if err != nil { return "" } oldARICertId, _ := legocertifier.MakeARICertID(oldCertX509) return oldARICertId }), } // 如果启用多进程模式,发送指令 if envMultiProc { type InData struct { Account *certacme.ACMEAccount `json:"account,omitempty"` Request *certacme.ObtainCertificateRequest `json:"request,omitempty"` } type OutData struct { Response *certacme.ObtainCertificateResponse `json:"response"` } msender := mproc.NewSender[InData, OutData]("certapply", ne.logger) moutput, err := msender.SendWithContext(execCtx.Context(), &InData{ Account: legoUser, Request: obtainReq, }) if err != nil { ne.logger.Warn("could not obtain certificate") return nil, err } if moutput.Response != nil { return moutput.Response, nil } else { panic("unreachable") } } // 初始化 ACME 客户端 legolog.Logger = certacme.NewLegoLogger(app.GetLogger()) legoClient, err := certacme.NewACMEClientWithAccount(legoUser, func(c *lego.Config) error { c.UserAgent = app.AppUserAgent c.Certificate.KeyType = legoKeyType c.Certificate.DisableCommonName = obtainReq.NoCommonName return nil }) if err != nil { ne.logger.Warn("could not initialize acme client") return nil, err } // 执行申请证书请求 obtainResp, err := legoClient.ObtainCertificate(execCtx.Context(), obtainReq) if err != nil { ne.logger.Warn("could not obtain certificate") return nil, err } return obtainResp, nil } func (ne *bizApplyNodeExecutor) setOuputsOfResult(execCtx *NodeExecutionContext, execRes *NodeExecutionResult, certificate *domain.Certificate, persistent bool) { if certificate != nil { key := "certificate" value := fmt.Sprintf("%s#%s", domain.CollectionNameCertificate, certificate.Id) if persistent { execRes.AddOutputWithPersistent(stateIOTypeRef, key, value, stateValTypeString) } else { execRes.AddOutput(stateIOTypeRef, key, value, stateValTypeString) } } } func (ne *bizApplyNodeExecutor) setVariablesOfResult(execCtx *NodeExecutionContext, execRes *NodeExecutionResult, certificate *domain.Certificate) { var vCommonName string var vSubjectAltNames string var vNotBefore time.Time var vNotAfter time.Time var vHoursLeft int32 var vDaysLeft int32 var vValidity bool if certificate != nil { vCommonName = strings.Split(certificate.SubjectAltNames, ";")[0] vSubjectAltNames = certificate.SubjectAltNames vNotBefore = certificate.ValidityNotBefore vNotAfter = certificate.ValidityNotAfter vHoursLeft = int32(math.Floor(time.Until(certificate.ValidityNotAfter).Hours())) vDaysLeft = int32(math.Floor(time.Until(certificate.ValidityNotAfter).Hours() / 24)) vValidity = certificate.ValidityNotAfter.After(time.Now()) } execRes.AddVariable(stateVarKeyCertificateDomain, vCommonName, stateValTypeString) execRes.AddVariable(stateVarKeyCertificateDomains, vSubjectAltNames, stateValTypeString) execRes.AddVariable(stateVarKeyCertificateCommonName, vCommonName, stateValTypeString) execRes.AddVariable(stateVarKeyCertificateSubjectAltNames, vSubjectAltNames, stateValTypeString) execRes.AddVariable(stateVarKeyCertificateNotBefore, vNotBefore, stateValTypeDateTime) execRes.AddVariable(stateVarKeyCertificateNotAfter, vNotAfter, stateValTypeDateTime) execRes.AddVariable(stateVarKeyCertificateHoursLeft, vHoursLeft, stateValTypeNumber) execRes.AddVariable(stateVarKeyCertificateDaysLeft, vDaysLeft, stateValTypeNumber) execRes.AddVariable(stateVarKeyCertificateValidity, vValidity, stateValTypeBoolean) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDomain, vCommonName, stateValTypeString) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDomains, vSubjectAltNames, stateValTypeString) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateCommonName, vCommonName, stateValTypeString) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateSubjectAltNames, vSubjectAltNames, stateValTypeString) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateNotBefore, vNotBefore, stateValTypeDateTime) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateNotAfter, vNotAfter, stateValTypeDateTime) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateHoursLeft, vHoursLeft, stateValTypeNumber) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDaysLeft, vDaysLeft, stateValTypeNumber) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateValidity, vValidity, stateValTypeBoolean) } func newBizApplyNodeExecutor() NodeExecutor { return &bizApplyNodeExecutor{ nodeExecutor: nodeExecutor{logger: slog.Default()}, accessRepo: repository.NewAccessRepository(), certificateRepo: repository.NewCertificateRepository(), wfoutputRepo: repository.NewWorkflowOutputRepository(), } } ================================================ FILE: internal/workflow/engine/executor_bizdeploy.go ================================================ package engine import ( "fmt" "log/slog" "maps" "strings" "github.com/certimate-go/certimate/internal/certmgmt" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/internal/repository" ) /** * Inputs: * - ref: "certificate": string * * Variables: * - "node.skipped": boolean */ type bizDeployNodeExecutor struct { nodeExecutor accessRepo accessRepository certificateRepo certificateRepository wfoutputRepo workflowOutputRepository } func (ne *bizDeployNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { execRes := newNodeExecutionResult(execCtx.Node) nodeCfg := execCtx.Node.Data.Config.AsBizDeploy() ne.logger.Info("ready to deploy certificate ...", slog.Any("config", nodeCfg)) // 查询上次执行结果 lastOutput, err := ne.getLastOutputArtifacts(execCtx) if err != nil { return execRes, err } else { if lastOutput != nil { ne.logger.Info(fmt.Sprintf("found last node output #%s record", lastOutput.RunId)) } } // 获取前序节点输出证书 var inputCertificate *domain.Certificate if inputState, ok := execCtx.inputs.Get(nodeCfg.CertificateOutputNodeId, "certificate"); ok { if inputStateValue, ok := inputState.Value.(string); ok { s := strings.Split(inputStateValue, "#") if len(s) == 2 { certificate, err := ne.certificateRepo.GetById(execCtx.Context(), s[1]) if err != nil { ne.logger.Warn("could not get input certificate") return execRes, err } inputCertificate = certificate } } } if inputCertificate == nil { return execRes, fmt.Errorf("invalid input certificate") } // 检测是否可以跳过本次执行 if lastOutput != nil && inputCertificate.CreatedAt.Before(lastOutput.UpdatedAt) { if skippable, reason := ne.checkCanSkip(execCtx, lastOutput); skippable { ne.logger.Info(fmt.Sprintf("skip this deployment, because %s", reason)) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyNodeSkipped, true, stateValTypeBoolean) return execRes, nil } else if reason != "" { ne.logger.Info(fmt.Sprintf("re-deploy, because %s", reason)) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyNodeSkipped, false, stateValTypeBoolean) } } else { execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyNodeSkipped, false, stateValTypeBoolean) } // 读取部署提供商授权 providerAccessConfig := make(map[string]any) if nodeCfg.ProviderAccessId != "" { if access, err := ne.accessRepo.GetById(execCtx.Context(), nodeCfg.ProviderAccessId); err != nil { return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.ProviderAccessId, err) } else { providerAccessConfig = access.Config } } // 部署证书 deployer := certmgmt.NewClient(certmgmt.WithLogger(ne.logger)) deployReq := &certmgmt.DeployCertificateRequest{ Provider: nodeCfg.Provider, ProviderAccessConfig: providerAccessConfig, ProviderExtendedConfig: nodeCfg.ProviderConfig, Certificate: inputCertificate.Certificate, PrivateKey: inputCertificate.PrivateKey, } if _, err := deployer.DeployCertificate(execCtx.Context(), deployReq); err != nil { ne.logger.Warn("could not deploy certificate") return execRes, err } // 节点输出 execRes.outputForced = true ne.logger.Info("deployment completed") return execRes, nil } func (ne *bizDeployNodeExecutor) getLastOutputArtifacts(execCtx *NodeExecutionContext) (*domain.WorkflowOutput, error) { lastOutput, err := ne.wfoutputRepo.GetByWorkflowIdAndNodeId(execCtx.Context(), execCtx.WorkflowId, execCtx.Node.Id) if err != nil && !domain.IsRecordNotFoundError(err) { return nil, fmt.Errorf("failed to get last output record of node #%s: %w", execCtx.Node.Id, err) } return lastOutput, nil } func (ne *bizDeployNodeExecutor) checkCanSkip(execCtx *NodeExecutionContext, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) { thisNodeCfg := execCtx.Node.Data.Config.AsBizDeploy() if lastOutput != nil && lastOutput.Succeeded { // 比较和上次部署时的关键配置(即影响证书部署的)参数是否一致 lastNodeCfg := lastOutput.NodeConfig.AsBizDeploy() if thisNodeCfg.ProviderAccessId != lastNodeCfg.ProviderAccessId { return false, "the configuration item 'ProviderAccessId' changed" } if !maps.Equal(thisNodeCfg.ProviderConfig, lastNodeCfg.ProviderConfig) { return false, "the configuration item 'ProviderConfig' changed" } if thisNodeCfg.SkipOnLastSucceeded { return true, "the last deployment already completed" } } return false, "" } func newBizDeployNodeExecutor() NodeExecutor { return &bizDeployNodeExecutor{ nodeExecutor: nodeExecutor{logger: slog.Default()}, accessRepo: repository.NewAccessRepository(), certificateRepo: repository.NewCertificateRepository(), wfoutputRepo: repository.NewWorkflowOutputRepository(), } } ================================================ FILE: internal/workflow/engine/executor_bizmonitor.go ================================================ package engine import ( "crypto/x509" "fmt" "log/slog" "math" "net" "net/http" "strconv" "strings" "time" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/internal/repository" xcertx509 "github.com/certimate-go/certimate/pkg/utils/cert/x509" xhttp "github.com/certimate-go/certimate/pkg/utils/http" xtls "github.com/certimate-go/certimate/pkg/utils/tls" ) /** * Variables: * - "certificate.commanName": string * - "certificate.subjectAltNames": string * - "certificate.notBefore": datetime * - "certificate.notAfter": datetime * - "certificate.hoursLeft": number * - "certificate.daysLeft": number * - "certificate.validity": boolean */ type bizMonitorNodeExecutor struct { nodeExecutor certificateRepo certificateRepository } func (ne *bizMonitorNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { execRes := newNodeExecutionResult(execCtx.Node) nodeCfg := execCtx.Node.Data.Config.AsBizMonitor() ne.logger.Info("ready to monitor certificate ...", slog.Any("config", nodeCfg)) targetAddr := net.JoinHostPort(nodeCfg.Host, strconv.Itoa(int(nodeCfg.Port))) if nodeCfg.Port == 0 { targetAddr = net.JoinHostPort(nodeCfg.Host, "443") } targetDomain := nodeCfg.Domain if targetDomain == "" { targetDomain = nodeCfg.Host } ne.logger.Info(fmt.Sprintf("retrieving certificate at %s (domain: %s)", targetAddr, targetDomain)) const MAX_ATTEMPTS = 3 const RETRY_INTERVAL = 2 * time.Second var err error var certs []*x509.Certificate for attempt := 0; attempt < MAX_ATTEMPTS; attempt++ { if attempt > 0 { ne.logger.Info(fmt.Sprintf("retry %d time(s) ...", attempt)) ctx := execCtx.Context() select { case <-ctx.Done(): return execRes, ctx.Err() case <-time.After(RETRY_INTERVAL): } } certs, err = ne.tryRetrievePeerCertificates(execCtx, targetAddr, targetDomain, nodeCfg.RequestPath) if err == nil { break } } if err != nil { ne.logger.Warn("could not retrieve certificate") return execRes, err } else { if len(certs) == 0 { ne.logger.Warn("no ssl certificates retrieved in http response") ne.setVariablesOfResult(execCtx, execRes, nil) } else { cert := certs[0] // 只取证书链中的第一个证书,即服务器证书 ne.logger.Info(fmt.Sprintf("ssl certificate retrieved (serial='%s', subject='%s', issuer='%s', not_before='%s', not_after='%s', sans='%s')", cert.SerialNumber, cert.Subject.String(), cert.Issuer.String(), cert.NotBefore.Format(time.RFC3339), cert.NotAfter.Format(time.RFC3339), strings.Join(xcertx509.GetSubjectAltNames(cert), ";")), ) ne.setVariablesOfResult(execCtx, execRes, cert) now := time.Now() isCertPeriodValid := now.Before(cert.NotAfter) && now.After(cert.NotBefore) isCertHostMatched := cert.VerifyHostname(targetDomain) == nil daysLeft := int32(math.Floor(time.Until(cert.NotAfter).Hours() / 24)) validated := isCertPeriodValid && isCertHostMatched if validated { ne.logger.Info(fmt.Sprintf("the certificate is valid, and will expire in %d day(s)", daysLeft)) } else { if !isCertHostMatched { ne.logger.Warn("the certificate is invalid, because it is not matched the host") } else if !isCertPeriodValid { ne.logger.Warn("the certificate is invalid, because it is either expired or not yet valid") } else { ne.logger.Warn("the certificate is invalid") } // 除了验证证书有效期,还要确保证书与域名匹配 execRes.AddVariable(stateVarKeyCertificateValidity, false, stateValTypeBoolean) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateValidity, false, stateValTypeBoolean) } } } ne.logger.Info("monitoring completed") return execRes, nil } func (ne *bizMonitorNodeExecutor) tryRetrievePeerCertificates(execCtx *NodeExecutionContext, addr, domain, requestPath string) ([]*x509.Certificate, error) { transport := xhttp.NewDefaultTransport() transport.DisableKeepAlives = true transport.TLSClientConfig = xtls.NewInsecureConfig() client := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, Timeout: 30 * time.Second, Transport: transport, } url := fmt.Sprintf("https://%s/%s", addr, strings.TrimLeft(requestPath, "/")) req, err := http.NewRequestWithContext(execCtx.Context(), http.MethodHead, url, nil) if err != nil { err = fmt.Errorf("failed to create http request: %w", err) ne.logger.Warn(err.Error()) return nil, err } req.Header.Set("Host", domain) req.Header.Set("User-Agent", app.AppUserAgent) resp, err := client.Do(req) if err != nil { err = fmt.Errorf("failed to send http request: %w", err) ne.logger.Warn(err.Error()) return nil, err } defer resp.Body.Close() if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 { return make([]*x509.Certificate, 0), nil } return resp.TLS.PeerCertificates, nil } func (ne *bizMonitorNodeExecutor) setVariablesOfResult(execCtx *NodeExecutionContext, execRes *NodeExecutionResult, certX509 *x509.Certificate) { var vCommonName string var vSubjectAltNames string var vNotBefore time.Time var vNotAfter time.Time var vHoursLeft int32 var vDaysLeft int32 var vValidity bool if certX509 != nil { vCommonName = certX509.Subject.CommonName vSubjectAltNames = strings.Join(xcertx509.GetSubjectAltNames(certX509), ";") vNotBefore = certX509.NotBefore vNotAfter = certX509.NotAfter vHoursLeft = int32(math.Floor(time.Until(certX509.NotAfter).Hours())) vDaysLeft = int32(math.Floor(time.Until(certX509.NotAfter).Hours() / 24)) vValidity = certX509.NotAfter.After(time.Now()) } execRes.AddVariable(stateVarKeyCertificateDomain, vCommonName, stateValTypeString) execRes.AddVariable(stateVarKeyCertificateDomains, vSubjectAltNames, stateValTypeString) execRes.AddVariable(stateVarKeyCertificateCommonName, vCommonName, stateValTypeString) execRes.AddVariable(stateVarKeyCertificateSubjectAltNames, vSubjectAltNames, stateValTypeString) execRes.AddVariable(stateVarKeyCertificateNotBefore, vNotBefore, stateValTypeDateTime) execRes.AddVariable(stateVarKeyCertificateNotAfter, vNotAfter, stateValTypeDateTime) execRes.AddVariable(stateVarKeyCertificateHoursLeft, vHoursLeft, stateValTypeNumber) execRes.AddVariable(stateVarKeyCertificateDaysLeft, vDaysLeft, stateValTypeNumber) execRes.AddVariable(stateVarKeyCertificateValidity, vValidity, stateValTypeBoolean) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDomain, vCommonName, stateValTypeString) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDomains, vSubjectAltNames, stateValTypeString) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateCommonName, vCommonName, stateValTypeString) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateSubjectAltNames, vSubjectAltNames, stateValTypeString) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateNotBefore, vNotBefore, stateValTypeDateTime) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateNotAfter, vNotAfter, stateValTypeDateTime) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateHoursLeft, vHoursLeft, stateValTypeNumber) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDaysLeft, vDaysLeft, stateValTypeNumber) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateValidity, vValidity, stateValTypeBoolean) } func newBizMonitorNodeExecutor() NodeExecutor { return &bizMonitorNodeExecutor{ nodeExecutor: nodeExecutor{logger: slog.Default()}, certificateRepo: repository.NewCertificateRepository(), } } ================================================ FILE: internal/workflow/engine/executor_biznotify.go ================================================ package engine import ( "fmt" "log/slog" "regexp" "strings" "time" "github.com/certimate-go/certimate/internal/notify" "github.com/certimate-go/certimate/internal/repository" ) type bizNotifyNodeExecutor struct { nodeExecutor accessRepo accessRepository settingsRepo settingsRepository } func (ne *bizNotifyNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { execRes := newNodeExecutionResult(execCtx.Node) nodeCfg := execCtx.Node.Data.Config.AsBizNotify() ne.logger.Info("ready to send notification ...", slog.Any("config", nodeCfg)) // 检测是否可以跳过本次执行 if skippable, reason := ne.checkCanSkip(execCtx); skippable { ne.logger.Info(fmt.Sprintf("skip this application, because %s", reason)) return execRes, nil } // 读取部署提供商授权 providerAccessConfig := make(map[string]any) if nodeCfg.ProviderAccessId != "" { if access, err := ne.accessRepo.GetById(execCtx.Context(), nodeCfg.ProviderAccessId); err != nil { return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.ProviderAccessId, err) } else { providerAccessConfig = access.Config } } // 渲染通知模板 reMustache := regexp.MustCompile(`\{\{\s*(\$[^\s]+)\s*\}\}`) reMustacheReplacer := func(match string) string { mustache := strings.TrimSpace(match[2 : len(match)-2]) if mustache == "" { return match } key := mustache[1:] if key == "" { return match } else if key == "now" { return time.Now().Format(time.RFC3339) } // TODO: 支持作用域变量 if state, ok := execCtx.variables.Get(key); ok { return state.ValueString() } return match } subject := reMustache.ReplaceAllStringFunc(nodeCfg.Subject, reMustacheReplacer) message := reMustache.ReplaceAllStringFunc(nodeCfg.Message, reMustacheReplacer) // 推送通知 notifier := notify.NewClient(notify.WithLogger(ne.logger)) notifyReq := ¬ify.SendNotificationRequest{ Provider: nodeCfg.Provider, ProviderAccessConfig: providerAccessConfig, ProviderExtendedConfig: nodeCfg.ProviderConfig, Subject: subject, Message: message, } if _, err := notifier.SendNotification(execCtx.Context(), notifyReq); err != nil { ne.logger.Warn("could not send notification") return execRes, err } ne.logger.Info("notification completed") return execRes, nil } func (ne *bizNotifyNodeExecutor) checkCanSkip(execCtx *NodeExecutionContext) (_skip bool, _reason string) { thisNodeCfg := execCtx.Node.Data.Config.AsBizNotify() if !thisNodeCfg.SkipOnAllPrevSkipped { return false, "" } var total, skipped int32 for _, variable := range execCtx.variables.All() { if variable.Scope != "" && variable.Key == stateVarKeyNodeSkipped { total++ if variable.Value == true { skipped++ } } } if total == 0 || skipped != total { return false, "" } return true, "all the previous nodes have been skipped" } func newBizNotifyNodeExecutor() NodeExecutor { return &bizNotifyNodeExecutor{ nodeExecutor: nodeExecutor{logger: slog.Default()}, accessRepo: repository.NewAccessRepository(), settingsRepo: repository.NewSettingsRepository(), } } ================================================ FILE: internal/workflow/engine/executor_bizupload.go ================================================ package engine import ( "crypto/ecdsa" "crypto/ed25519" "crypto/rsa" "crypto/tls" "fmt" "log/slog" "math" "os" "strings" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/internal/repository" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) /** * Outputs: * - ref: "certificate": string * * Variables: * - "node.skipped": boolean * - "certificate.commanName": string * - "certificate.subjectAltNames": string * - "certificate.notBefore": datetime * - "certificate.notAfter": datetime * - "certificate.hoursLeft": number * - "certificate.daysLeft": number * - "certificate.validity": boolean */ type bizUploadNodeExecutor struct { nodeExecutor certificateRepo certificateRepository wfoutputRepo workflowOutputRepository } const ( BizUploadSourceForm = "form" BizUploadSourceLocal = "local" BizUploadSourceURL = "url" ) func (ne *bizUploadNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { execRes := newNodeExecutionResult(execCtx.Node) nodeCfg := execCtx.Node.Data.Config.AsBizUpload() ne.logger.Info("ready to upload certiticate ...", slog.Any("config", nodeCfg)) // 查询上次执行结果 lastOutput, lastCertificate, err := ne.getLastOutputArtifacts(execCtx) if err != nil { return execRes, err } else { if lastOutput != nil { ne.logger.Info(fmt.Sprintf("found last node output #%s record", lastOutput.RunId)) } if lastCertificate != nil { ne.setOuputsOfResult(execCtx, execRes, lastCertificate, false) ne.setVariablesOfResult(execCtx, execRes, lastCertificate) ne.logger.Info(fmt.Sprintf("found last certificate #%s record", lastCertificate.Id)) } } // 检测是否可以跳过本次执行 if skippable, reason := ne.checkCanSkip(execCtx, lastOutput, lastCertificate); skippable { ne.logger.Info(fmt.Sprintf("skip this uploading, because %s", reason)) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyNodeSkipped, true, stateValTypeBoolean) return execRes, nil } else if reason != "" { ne.logger.Info(fmt.Sprintf("re-upload, because %s", reason)) } else if lastCertificate != nil { ne.logger.Info("no found last uploaded certificate, begin to upload") } else { ne.logger.Info("try to upload") } // 获取证书及私钥 var certPEM, privkeyPEM string switch nodeCfg.Source { case BizUploadSourceForm: { certPEM = nodeCfg.Certificate privkeyPEM = nodeCfg.PrivateKey } case BizUploadSourceLocal: { certData, err := os.ReadFile(nodeCfg.Certificate) if err != nil { return execRes, fmt.Errorf("failed to read certificate file from local path: %w", err) } else { certPEM = string(certData) } privkeyData, err := os.ReadFile(nodeCfg.PrivateKey) if err != nil { return execRes, fmt.Errorf("failed to read private key file from local path: %w", err) } else { privkeyPEM = string(privkeyData) } } case BizUploadSourceURL: { client := resty.New() client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) certResp, err := client.NewRequest().Get(nodeCfg.Certificate) if err != nil || certResp.IsError() { return execRes, fmt.Errorf("failed to download certificate from URL: %w", err) } else { certPEM = string(certResp.Body()) } privkeyResp, err := client.NewRequest().Get(nodeCfg.PrivateKey) if err != nil || privkeyResp.IsError() { return execRes, fmt.Errorf("failed to download private key from URL: %w", err) } else { privkeyPEM = string(privkeyResp.Body()) } } default: return execRes, fmt.Errorf("unsupported upload source: '%s'", nodeCfg.Source) } // 验证证书 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return execRes, err } else if certX509.NotAfter.Before(time.Now()) { ne.logger.Warn(fmt.Sprintf("the uploaded certificate has expired at %s", certX509.NotAfter.UTC().Format(time.RFC3339))) } // 验证私钥 privkey, err := xcert.ParsePrivateKeyFromPEM(privkeyPEM) if err != nil { return nil, err } else { matched := false switch pub := certX509.PublicKey.(type) { case *rsa.PublicKey: p, ok := privkey.(*rsa.PrivateKey) matched = ok && pub.Equal(p.Public()) case *ecdsa.PublicKey: p, ok := privkey.(*ecdsa.PrivateKey) matched = ok && pub.Equal(p.Public()) case ed25519.PublicKey: p, ok := privkey.(ed25519.PrivateKey) matched = ok && pub.Equal(p.Public()) default: matched = false } if !matched { return nil, fmt.Errorf("the uploaded private key does not match the uploaded certificate") } } // 二次检测是否可以跳过执行 if lastCertificate != nil { if xcert.EqualCertificatesFromPEM(certPEM, lastCertificate.Certificate) { ne.logger.Info("skip this uploading, because the last uploaded certificate already exists") return execRes, nil } } // 保存证书实体 certificate := &domain.Certificate{ Source: domain.CertificateSourceTypeUpload, WorkflowId: execCtx.WorkflowId, WorkflowRunId: execCtx.RunId, WorkflowNodeId: execCtx.Node.Id, } certificate.PopulateFromPEM(certPEM, privkeyPEM) if certificate, err := ne.certificateRepo.Save(execCtx.Context(), certificate); err != nil { ne.logger.Warn("could not save certificate") return execRes, err } else { ne.logger.Info("certificate saved", slog.String("recordId", certificate.Id)) } // 节点输出 ne.setOuputsOfResult(execCtx, execRes, certificate, true) ne.setVariablesOfResult(execCtx, execRes, certificate) ne.logger.Info("uploading completed") return execRes, nil } func (ne *bizUploadNodeExecutor) getLastOutputArtifacts(execCtx *NodeExecutionContext) (*domain.WorkflowOutput, *domain.Certificate, error) { lastOutput, err := ne.wfoutputRepo.GetByWorkflowIdAndNodeId(execCtx.Context(), execCtx.WorkflowId, execCtx.Node.Id) if err != nil && !domain.IsRecordNotFoundError(err) { return nil, nil, fmt.Errorf("failed to get last output record of node #%s: %w", execCtx.Node.Id, err) } if lastOutput != nil { lastCertificate, err := ne.certificateRepo.GetByWorkflowRunIdAndNodeId(execCtx.Context(), lastOutput.RunId, lastOutput.NodeId) if err != nil && !domain.IsRecordNotFoundError(err) { return lastOutput, nil, fmt.Errorf("failed to get last certificate record of node #%s: %w", execCtx.Node.Id, err) } return lastOutput, lastCertificate, nil } return lastOutput, nil, nil } func (ne *bizUploadNodeExecutor) checkCanSkip(execCtx *NodeExecutionContext, lastOutput *domain.WorkflowOutput, lastCertificate *domain.Certificate) (_skip bool, _reason string) { thisNodeCfg := execCtx.Node.Data.Config.AsBizUpload() if lastOutput != nil && lastOutput.Succeeded { // 比较和上次上传时的关键配置(即影响证书上传的)参数是否一致 lastNodeCfg := lastOutput.NodeConfig.AsBizUpload() if thisNodeCfg.Source != lastNodeCfg.Source { return false, "the configuration item 'Source' changed" } switch thisNodeCfg.Source { case BizUploadSourceForm: if strings.TrimSpace(thisNodeCfg.Certificate) != strings.TrimSpace(lastNodeCfg.Certificate) { return false, "the configuration item 'Certificate' changed" } if strings.TrimSpace(thisNodeCfg.PrivateKey) != strings.TrimSpace(lastNodeCfg.PrivateKey) { return false, "the configuration item 'PrivateKey' changed" } default: // 本地或远程文件来源,需实际下载后才能比较 return false, "" } } if lastCertificate != nil { return true, "the last uploaded certificate already exists" } return false, "" } func (ne *bizUploadNodeExecutor) setOuputsOfResult(execCtx *NodeExecutionContext, execRes *NodeExecutionResult, certificate *domain.Certificate, persistent bool) { if certificate != nil { key := "certificate" value := fmt.Sprintf("%s#%s", domain.CollectionNameCertificate, certificate.Id) if persistent { execRes.AddOutputWithPersistent(stateIOTypeRef, key, value, stateValTypeString) } else { execRes.AddOutput(stateIOTypeRef, key, value, stateValTypeString) } } } func (ne *bizUploadNodeExecutor) setVariablesOfResult(execCtx *NodeExecutionContext, execRes *NodeExecutionResult, certificate *domain.Certificate) { var vCommonName string var vSubjectAltNames string var vNotBefore time.Time var vNotAfter time.Time var vHoursLeft int32 var vDaysLeft int32 var vValidity bool if certificate != nil { vCommonName = strings.Split(certificate.SubjectAltNames, ";")[0] vSubjectAltNames = certificate.SubjectAltNames vNotBefore = certificate.ValidityNotBefore vNotAfter = certificate.ValidityNotAfter vHoursLeft = int32(math.Floor(time.Until(certificate.ValidityNotAfter).Hours())) vDaysLeft = int32(math.Floor(time.Until(certificate.ValidityNotAfter).Hours() / 24)) vValidity = certificate.ValidityNotAfter.After(time.Now()) } execRes.AddVariable(stateVarKeyCertificateDomain, vCommonName, stateValTypeString) execRes.AddVariable(stateVarKeyCertificateDomains, vSubjectAltNames, stateValTypeString) execRes.AddVariable(stateVarKeyCertificateCommonName, vCommonName, stateValTypeString) execRes.AddVariable(stateVarKeyCertificateSubjectAltNames, vSubjectAltNames, stateValTypeString) execRes.AddVariable(stateVarKeyCertificateNotBefore, vNotBefore, stateValTypeDateTime) execRes.AddVariable(stateVarKeyCertificateNotAfter, vNotAfter, stateValTypeDateTime) execRes.AddVariable(stateVarKeyCertificateHoursLeft, vHoursLeft, stateValTypeNumber) execRes.AddVariable(stateVarKeyCertificateDaysLeft, vDaysLeft, stateValTypeNumber) execRes.AddVariable(stateVarKeyCertificateValidity, vValidity, stateValTypeBoolean) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDomain, vCommonName, stateValTypeString) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDomains, vSubjectAltNames, stateValTypeString) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateCommonName, vCommonName, stateValTypeString) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateSubjectAltNames, vSubjectAltNames, stateValTypeString) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateNotBefore, vNotBefore, stateValTypeDateTime) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateNotAfter, vNotAfter, stateValTypeDateTime) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateHoursLeft, vHoursLeft, stateValTypeNumber) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateDaysLeft, vDaysLeft, stateValTypeNumber) execRes.AddVariableWithScope(execCtx.Node.Id, stateVarKeyCertificateValidity, vValidity, stateValTypeBoolean) } func newBizUploadNodeExecutor() NodeExecutor { return &bizUploadNodeExecutor{ nodeExecutor: nodeExecutor{logger: slog.Default()}, certificateRepo: repository.NewCertificateRepository(), wfoutputRepo: repository.NewWorkflowOutputRepository(), } } ================================================ FILE: internal/workflow/engine/executor_condition.go ================================================ package engine import ( "errors" "fmt" "log/slog" "github.com/samber/lo" ) type conditionNodeExecutor struct { nodeExecutor } func (ne *conditionNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { var engine *workflowEngine if we, ok := execCtx.engine.(*workflowEngine); !ok { panic("unreachable") } else { engine = we } execRes := newNodeExecutionResult(execCtx.Node) errs := make([]error, 0) blocks := lo.Filter(execCtx.Node.Blocks, func(n *Node, _ int) bool { return n.Type == NodeTypeBranchBlock }) for _, node := range blocks { ctx := execCtx.Context() select { case <-ctx.Done(): return execRes, ctx.Err() default: } err := engine.executeNode(execCtx.Clone(), node) if err != nil { if errors.Is(err, ErrTerminated) { return execRes, err } errs = append(errs, err) } } if len(errs) > 0 { return execRes, fmt.Errorf("%w: %w", ErrBlocksException, errors.Join(errs...)) } return execRes, nil } func newConditionNodeExecutor() NodeExecutor { return &conditionNodeExecutor{ nodeExecutor: nodeExecutor{logger: slog.Default()}, } } type branchBlockNodeExecutor struct { nodeExecutor } func (ne *branchBlockNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { execRes := newNodeExecutionResult(execCtx.Node) nodeCfg := execCtx.Node.Data.Config.AsBranchBlock() if nodeCfg.Expression == nil { ne.logger.Info("enter this branch without any conditions") } else { variables := lo.Reduce(execCtx.variables.All(), func(acc map[string]map[string]any, state VariableState, _ int) map[string]map[string]any { if _, ok := acc[state.Scope]; !ok { acc[state.Scope] = make(map[string]any) } // 这里需要把所有值都转换为字符串形式,因为 Expression.Eval 仅支持字符串类型的值 acc[state.Scope][state.Key] = state.ValueString() return acc }, make(map[string]map[string]any)) rs, err := nodeCfg.Expression.Eval(variables) if err != nil { ne.logger.Warn(fmt.Sprintf("failed to eval expr: %+v", err)) return execRes, err } if rs.Value == false { ne.logger.Info("skip this branch, because condition not met") return execRes, nil } else { ne.logger.Info("enter this branch, because condition met") } } if engine, ok := execCtx.engine.(*workflowEngine); !ok { panic("unreachable") } else { if err := engine.executeBlocks(execCtx.Clone(), execCtx.Node.Blocks); err != nil { return execRes, fmt.Errorf("%w: %w", ErrBlocksException, err) } } return execRes, nil } func newBranchBlockNodeExecutor() NodeExecutor { return &branchBlockNodeExecutor{ nodeExecutor: nodeExecutor{logger: slog.Default()}, } } ================================================ FILE: internal/workflow/engine/executor_delay.go ================================================ package engine import ( "fmt" "log/slog" "time" xwait "github.com/certimate-go/certimate/pkg/utils/wait" ) type delayNodeExecutor struct { nodeExecutor } func (ne *delayNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { execRes := newNodeExecutionResult(execCtx.Node) nodeCfg := execCtx.Node.Data.Config.AsDelay() ne.logger.Info(fmt.Sprintf("delay for %d second(s) before continuing ...", nodeCfg.Wait)) xwait.DelayWithContext(execCtx.Context(), time.Duration(nodeCfg.Wait)*time.Second) return execRes, nil } func newDelayNodeExecutor() NodeExecutor { return &delayNodeExecutor{ nodeExecutor: nodeExecutor{logger: slog.Default()}, } } ================================================ FILE: internal/workflow/engine/executor_end.go ================================================ package engine import ( "log/slog" ) type endNodeExecutor struct { nodeExecutor } func (ne *endNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { execRes := newNodeExecutionResult(execCtx.Node) execRes.Terminated = true ne.logger.Info("the workflow is ending") return execRes, nil } func newEndNodeExecutor() NodeExecutor { return &endNodeExecutor{ nodeExecutor: nodeExecutor{logger: slog.Default()}, } } ================================================ FILE: internal/workflow/engine/executor_start.go ================================================ package engine import ( "log/slog" ) type startNodeExecutor struct { nodeExecutor } func (ne *startNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { execRes := newNodeExecutionResult(execCtx.Node) ne.logger.Info("the workflow is starting") return execRes, nil } func newStartNodeExecutor() NodeExecutor { return &startNodeExecutor{ nodeExecutor: nodeExecutor{logger: slog.Default()}, } } ================================================ FILE: internal/workflow/engine/executor_trycatch.go ================================================ package engine import ( "errors" "fmt" "log/slog" "github.com/samber/lo" ) type tryCatchNodeExecutor struct { nodeExecutor } func (ne *tryCatchNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { var engine *workflowEngine if we, ok := execCtx.engine.(*workflowEngine); !ok { panic("unreachable") } else { engine = we } execRes := newNodeExecutionResult(execCtx.Node) tryErrs := make([]error, 0) tryBlocks := lo.Filter(execCtx.Node.Blocks, func(n *Node, _ int) bool { return n.Type == NodeTypeTryBlock }) for _, node := range tryBlocks { ctx := execCtx.Context() select { case <-ctx.Done(): return execRes, ctx.Err() default: } err := engine.executeNode(execCtx.Clone(), node) if err != nil { if errors.Is(err, ErrTerminated) { return execRes, err } tryErrs = append(tryErrs, err) } } if len(tryErrs) > 0 { catchErrs := make([]error, 0) catchBlocks := lo.Filter(execCtx.Node.Blocks, func(n *Node, _ int) bool { return n.Type == NodeTypeCatchBlock }) for _, node := range catchBlocks { select { case <-execCtx.Context().Done(): return execRes, execCtx.Context().Err() default: } err := engine.executeNode(execCtx.Clone(), node) if err != nil { if errors.Is(err, ErrTerminated) { return execRes, err } catchErrs = append(catchErrs, err) } } errs := make([]error, 0) errs = append(errs, tryErrs...) errs = append(errs, catchErrs...) return execRes, fmt.Errorf("%w: %w", ErrBlocksException, errors.Join(errs...)) } return execRes, nil } func newTryCatchNodeExecutor() NodeExecutor { return &tryCatchNodeExecutor{ nodeExecutor: nodeExecutor{logger: slog.Default()}, } } type tryBlockNodeExecutor struct { nodeExecutor } func (ne *tryBlockNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { var engine *workflowEngine if we, ok := execCtx.engine.(*workflowEngine); !ok { panic("unreachable") } else { engine = we } execRes := newNodeExecutionResult(execCtx.Node) if err := engine.executeBlocks(execCtx.Clone(), execCtx.Node.Blocks); err != nil { return execRes, fmt.Errorf("%w: %w", ErrBlocksException, err) } return execRes, nil } func newTryBlockNodeExecutor() NodeExecutor { return &tryBlockNodeExecutor{ nodeExecutor: nodeExecutor{logger: slog.Default()}, } } type catchBlockNodeExecutor struct { nodeExecutor } func (ne *catchBlockNodeExecutor) Execute(execCtx *NodeExecutionContext) (*NodeExecutionResult, error) { execRes := newNodeExecutionResult(execCtx.Node) var engine *workflowEngine if we, ok := execCtx.engine.(*workflowEngine); !ok { panic("unreachable") } else { engine = we } if err := engine.executeBlocks(execCtx.Clone(), execCtx.Node.Blocks); err != nil { return execRes, fmt.Errorf("%w: %w", ErrBlocksException, err) } return execRes, nil } func newCatchBlockNodeExecutor() NodeExecutor { return &catchBlockNodeExecutor{ nodeExecutor: nodeExecutor{logger: slog.Default()}, } } ================================================ FILE: internal/workflow/engine/logger.go ================================================ package engine import ( "log/slog" ) type withLogger interface { SetLogger(logger *slog.Logger) } ================================================ FILE: internal/workflow/engine/models.go ================================================ package engine import ( "github.com/certimate-go/certimate/internal/domain" ) type Node = domain.WorkflowNode type NodeType = domain.WorkflowNodeType const ( NodeTypeStart = domain.WorkflowNodeTypeStart NodeTypeEnd = domain.WorkflowNodeTypeEnd NodeTypeCondition = domain.WorkflowNodeTypeCondition NodeTypeBranchBlock = domain.WorkflowNodeTypeBranchBlock NodeTypeTryCatch = domain.WorkflowNodeTypeTryCatch NodeTypeTryBlock = domain.WorkflowNodeTypeTryBlock NodeTypeCatchBlock = domain.WorkflowNodeTypeCatchBlock NodeTypeDelay = domain.WorkflowNodeTypeDelay NodeTypeBizApply = domain.WorkflowNodeTypeBizApply NodeTypeBizUpload = domain.WorkflowNodeTypeBizUpload NodeTypeBizMonitor = domain.WorkflowNodeTypeBizMonitor NodeTypeBizDeploy = domain.WorkflowNodeTypeBizDeploy NodeTypeBizNotify = domain.WorkflowNodeTypeBizNotify ) type Graph = domain.WorkflowGraph ================================================ FILE: internal/workflow/engine/state.go ================================================ package engine import ( "fmt" "slices" "strconv" "sync" "time" ) type VariableState struct { Scope string // 零值时表示全局的,否则表示指定节点的 Key string Value any ValueType string } func (s VariableState) ValueString() string { switch s.ValueType { case stateValTypeString: return fmt.Sprintf("%s", s.Value) case stateValTypeNumber: return fmt.Sprintf("%d", s.Value) case stateValTypeBoolean: return strconv.FormatBool(s.Value.(bool)) case stateValTypeDateTime: valueAsTime := s.Value.(time.Time) if valueAsTime.IsZero() { return "-" } return valueAsTime.Format(time.RFC3339) default: return fmt.Sprintf("[%s]%v", s.ValueType, s.Value) } } type VariableManager interface { All() []VariableState Erase() Add(entry VariableState) Set(name string, value any, valueType string) SetScoped(scope string, name string, value any, valueType string) Get(name string) (*VariableState, bool) GetScoped(scope string, key string) (*VariableState, bool) Take(key string) (*VariableState, bool) TakeScoped(scope string, key string) (*VariableState, bool) Remove(key string) bool RemoveScoped(scope string, key string) bool } type variableManager struct { statesMtx sync.RWMutex states []VariableState } var _ VariableManager = (*variableManager)(nil) func (m *variableManager) All() []VariableState { m.statesMtx.RLock() defer m.statesMtx.RUnlock() if m.states == nil { return make([]VariableState, 0) } return slices.Clone(m.states) } func (m *variableManager) Erase() { m.statesMtx.Lock() defer m.statesMtx.Unlock() m.states = make([]VariableState, 0) } func (m *variableManager) Add(state VariableState) { m.statesMtx.Lock() defer m.statesMtx.Unlock() if m.states == nil { m.states = make([]VariableState, 0) } for i, item := range m.states { if item.Scope == state.Scope && item.Key == state.Key { m.states[i] = state return } } m.states = append(m.states, state) } func (m *variableManager) Set(key string, value any, valueType string) { m.SetScoped("", key, value, valueType) } func (m *variableManager) SetScoped(scope string, key string, value any, valueType string) { m.Add(VariableState{Scope: scope, Key: key, Value: value, ValueType: valueType}) } func (m *variableManager) Get(key string) (*VariableState, bool) { return m.GetScoped("", key) } func (m *variableManager) GetScoped(scope string, key string) (*VariableState, bool) { m.statesMtx.RLock() defer m.statesMtx.RUnlock() if m.states == nil { return nil, false } for _, item := range m.states { if item.Scope == scope && item.Key == key { return &item, true } } return nil, false } func (m *variableManager) Take(key string) (*VariableState, bool) { return m.TakeScoped("", key) } func (m *variableManager) TakeScoped(scope string, key string) (*VariableState, bool) { m.statesMtx.Lock() defer m.statesMtx.Unlock() if m.states == nil { return nil, false } for i, item := range m.states { if item.Scope == scope && item.Key == key { m.states = slices.Delete(m.states, i, i+1) return &item, true } } return nil, false } func (m *variableManager) Remove(key string) bool { return m.RemoveScoped("", key) } func (m *variableManager) RemoveScoped(scope string, key string) bool { _, ok := m.TakeScoped(scope, key) return ok } func newVariableManager() VariableManager { return &variableManager{ states: make([]VariableState, 0), } } type InOutState struct { NodeId string Type string Name string Value any ValueType string Persistent bool } func (s InOutState) ValueString() string { switch s.ValueType { case stateValTypeString: return s.Value.(string) case stateValTypeNumber: return fmt.Sprintf("%d", s.Value) case stateValTypeBoolean: return strconv.FormatBool(s.Value.(bool)) default: return fmt.Sprintf("%v", s.Value) } } type InOutManager interface { All() []InOutState Erase() Add(state InOutState) Set(nodeId string, stype string, name string, value any, valueType string, persistent bool) Get(nodeId string, name string) (*InOutState, bool) Take(nodeId string, name string) (*InOutState, bool) Remove(nodeId string, name string) bool } type inoutManager struct { statesMtx sync.RWMutex states []InOutState } var _ InOutManager = (*inoutManager)(nil) func (m *inoutManager) All() []InOutState { m.statesMtx.RLock() defer m.statesMtx.RUnlock() if m.states == nil { return make([]InOutState, 0) } return slices.Clone(m.states) } func (m *inoutManager) Erase() { m.statesMtx.Lock() defer m.statesMtx.Unlock() m.states = make([]InOutState, 0) } func (m *inoutManager) Add(state InOutState) { m.statesMtx.Lock() defer m.statesMtx.Unlock() if m.states == nil { m.states = make([]InOutState, 0) } for i, item := range m.states { if item.NodeId == state.NodeId && item.Name == state.Name { m.states[i] = state return } } m.states = append(m.states, state) } func (m *inoutManager) Set(nodeId string, stype string, name string, value any, valueType string, persistent bool) { m.Add(InOutState{NodeId: nodeId, Type: stype, Name: name, Value: value, ValueType: valueType, Persistent: persistent}) } func (m *inoutManager) Get(nodeId string, name string) (*InOutState, bool) { m.statesMtx.RLock() defer m.statesMtx.RUnlock() if m.states == nil { return nil, false } for _, item := range m.states { if item.NodeId == nodeId && item.Name == name { return &item, true } } return nil, false } func (m *inoutManager) Take(nodeId string, name string) (*InOutState, bool) { m.statesMtx.Lock() defer m.statesMtx.Unlock() if m.states == nil { return nil, false } for i, item := range m.states { if item.NodeId == nodeId && item.Name == name { m.states = slices.Delete(m.states, i, i+1) return &item, true } } return nil, false } func (m *inoutManager) Remove(nodeId string, name string) bool { _, ok := m.Take(nodeId, name) return ok } func newInOutManager() InOutManager { return &inoutManager{ states: make([]InOutState, 0), } } const ( stateValTypeBoolean = "boolean" stateValTypeDateTime = "datetime" stateValTypeNumber = "number" stateValTypeString = "string" ) const ( stateIOTypeRef = "ref" ) const ( stateVarKeyWorkflowId = "workflow.id" // ValueType: "string" stateVarKeyWorkflowName = "workflow.name" // ValueType: "string" stateVarKeyRunId = "run.id" // ValueType: "string" stateVarKeyRunTrigger = "run.trigger" // ValueType: "string" stateVarKeyNodeId = "node.id" // ValueType: "string" stateVarKeyNodeName = "node.name" // ValueType: "string" stateVarKeyNodeSkipped = "node.skipped" // ValueType: "boolean" stateVarKeyErrorNodeId = "error.nodeId" // ValueType: "string" stateVarKeyErrorNodeName = "error.nodeName" // ValueType: "string" stateVarKeyErrorMessage = "error.message" // ValueType: "string" stateVarKeyCertificateDomain = "certificate.domain" // 已废弃,仅为兼容旧版而保留,请使用 [stateVarKeyCertificateCommonName] stateVarKeyCertificateDomains = "certificate.domains" // 已废弃,仅为兼容旧版而保留,请使用 [stateVarKeyCertificateSubjectAltNames] stateVarKeyCertificateCommonName = "certificate.commonName" // ValueType: "string" stateVarKeyCertificateSubjectAltNames = "certificate.subjectAltNames" // ValueType: "string" stateVarKeyCertificateNotBefore = "certificate.notBefore" // ValueType: "datetime" stateVarKeyCertificateNotAfter = "certificate.notAfter" // ValueType: "datetime" stateVarKeyCertificateHoursLeft = "certificate.hoursLeft" // ValueType: "number" stateVarKeyCertificateDaysLeft = "certificate.daysLeft" // ValueType: "number" stateVarKeyCertificateValidity = "certificate.validity" // ValueType: "boolean" ) ================================================ FILE: internal/workflow/pbhook.go ================================================ package workflow import ( "context" "fmt" "github.com/pocketbase/pocketbase/core" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/internal/domain" ) func registerWorkflowRecordEvents() { pb := app.GetApp() pb.OnRecordCreateRequest(domain.CollectionNameWorkflow).BindFunc(func(e *core.RecordRequestEvent) error { if err := e.Next(); err != nil { return err } if err := onWorkflowRecordCreateOrUpdate(e.Request.Context(), e.Record); err != nil { app.GetLogger().Error(err.Error()) return err } return nil }) pb.OnRecordUpdateRequest(domain.CollectionNameWorkflow).BindFunc(func(e *core.RecordRequestEvent) error { if err := e.Next(); err != nil { return err } if err := onWorkflowRecordCreateOrUpdate(e.Request.Context(), e.Record); err != nil { app.GetLogger().Error(err.Error()) return err } return nil }) pb.OnRecordDeleteRequest(domain.CollectionNameWorkflow).BindFunc(func(e *core.RecordRequestEvent) error { if err := e.Next(); err != nil { return err } if err := onWorkflowRecordDelete(e.Request.Context(), e.Record); err != nil { app.GetLogger().Error(err.Error()) return err } return nil }) } func onWorkflowRecordCreateOrUpdate(_ context.Context, record *core.Record) error { scheduler := app.GetScheduler() // 向数据库插入/更新时,同时更新定时任务 enabled := record.GetBool("enabled") trigger := record.GetString("trigger") triggerCron := record.GetString("triggerCron") // 如果非定时触发或未启用,移除定时任务 if !enabled || trigger != string(domain.WorkflowTriggerTypeScheduled) { scheduler.Remove(fmt.Sprintf("workflow#%s", record.Id)) return nil } // 反之,重新添加定时任务 if err := registerWorkflowJob(thisSvcInst(), record.Id, triggerCron); err != nil { return err } return nil } func onWorkflowRecordDelete(_ context.Context, record *core.Record) error { scheduler := app.GetScheduler() // 从数据库删除时,同时移除定时任务 jobId := fmt.Sprintf("workflow#%s", record.Id) scheduler.Remove(jobId) return nil } ================================================ FILE: internal/workflow/pbjob.go ================================================ package workflow import ( "context" "fmt" "log/slog" "github.com/pocketbase/pocketbase/tools/cron" "github.com/samber/lo" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/internal/domain/dtos" ) func registerWorkflowJob(workflowSrv *WorkflowService, workflowId string, triggerCron string) error { scheduler := app.GetScheduler() jobId := fmt.Sprintf("workflow#%s", workflowId) job, _ := lo.Find(scheduler.Jobs(), func(j *cron.Job) bool { return j.Id() == jobId }) if job != nil && job.Expression() == triggerCron { return nil } err := scheduler.Add(jobId, triggerCron, func() { app.GetLogger().Info(fmt.Sprintf("workflow #%s is triggered ...", workflowId)) _, err := workflowSrv.StartRun(context.Background(), &dtos.WorkflowStartRunReq{ WorkflowId: workflowId, RunTrigger: domain.WorkflowTriggerTypeScheduled, }) if err != nil { app.GetLogger().Warn(fmt.Sprintf("failed to start scheduled run for workflow #%s", workflowId), slog.Any("error", err)) } }) if err != nil { app.GetLogger().Error(fmt.Sprintf("failed to register cron job for workflow #%s", workflowId), slog.Any("error", err)) return fmt.Errorf("failed to add cron job: %w", err) } app.GetLogger().Info(fmt.Sprintf("registered cron job for workflow #%s", workflowId), slog.String("cron", triggerCron)) return nil } ================================================ FILE: internal/workflow/service.go ================================================ package workflow import ( "context" "errors" "fmt" "log/slog" "time" "github.com/pocketbase/dbx" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/internal/domain/dtos" "github.com/certimate-go/certimate/internal/workflow/dispatcher" ) type WorkflowService struct { dispatcher dispatcher.WorkflowDispatcher workflowRepo workflowRepository workflowRunRepo workflowRunRepository settingsRepo settingsRepository } func NewWorkflowService(workflowRepo workflowRepository, workflowRunRepo workflowRunRepository, settingsRepo settingsRepository) *WorkflowService { srv := &WorkflowService{ dispatcher: dispatcher.GetSingletonDispatcher(), workflowRepo: workflowRepo, workflowRunRepo: workflowRunRepo, settingsRepo: settingsRepo, } return srv } func (s *WorkflowService) InitSchedule(ctx context.Context) error { // 每日清理工作流运行历史 app.GetScheduler().MustAdd("cleanupWorkflowHistoryRuns", "0 0 * * *", func() { s.cleanupHistoryRuns(context.Background()) }) // 初始化工作流调度器 if err := s.dispatcher.Bootup(ctx); err != nil { panic(err) } // 注册工作流后台任务 { workflows, err := s.workflowRepo.ListEnabledScheduled(ctx) if err != nil { return err } var errs []error for _, workflow := range workflows { if err := registerWorkflowJob(s, workflow.Id, workflow.TriggerCron); err != nil { errs = append(errs, err) } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (s *WorkflowService) GetStatistics(ctx context.Context) (*dtos.WorkflowStatisticsResp, error) { stats := s.dispatcher.GetStatistics() return &dtos.WorkflowStatisticsResp{ Concurrency: stats.Concurrency, PendingRunIds: stats.PendingRunIds, ProcessingRunIds: stats.ProcessingRunIds, }, nil } func (s *WorkflowService) StartRun(ctx context.Context, req *dtos.WorkflowStartRunReq) (*dtos.WorkflowStartRunResp, error) { workflow, err := s.workflowRepo.GetById(ctx, req.WorkflowId) if err != nil { return nil, err } if req.RunTrigger == domain.WorkflowTriggerTypeManual && (workflow.LastRunStatus == domain.WorkflowRunStatusTypePending || workflow.LastRunStatus == domain.WorkflowRunStatusTypeProcessing) { return nil, errors.New("workflow is already pending or processing") } else if workflow.GraphContent == nil { return nil, errors.New("workflow graph content is empty") } else if err := workflow.GraphContent.Verify(); err != nil { return nil, fmt.Errorf("workflow graph content is invalid: %w", err) } workflowRun := &domain.WorkflowRun{ WorkflowId: workflow.Id, Status: domain.WorkflowRunStatusTypePending, Trigger: req.RunTrigger, StartedAt: time.Now(), Graph: workflow.GraphContent.Clone(), } if resp, err := s.workflowRunRepo.Save(ctx, workflowRun); err != nil { return nil, err } else { workflowRun = resp } if err := s.dispatcher.Start(ctx, workflowRun.Id); err != nil { return nil, err } return &dtos.WorkflowStartRunResp{RunId: workflowRun.Id}, nil } func (s *WorkflowService) CancelRun(ctx context.Context, req *dtos.WorkflowCancelRunReq) (*dtos.WorkflowCancelRunResp, error) { workflow, err := s.workflowRepo.GetById(ctx, req.WorkflowId) if err != nil { return nil, err } workflowRun, err := s.workflowRunRepo.GetById(ctx, req.RunId) if err != nil { return nil, err } else if workflowRun.WorkflowId != workflow.Id { return nil, errors.New("workflow run not found") } else if workflowRun.Status != domain.WorkflowRunStatusTypePending && workflowRun.Status != domain.WorkflowRunStatusTypeProcessing { return nil, errors.New("workflow run is not pending or processing") } if err := s.dispatcher.Cancel(ctx, workflowRun.Id); err != nil { return nil, err } return &dtos.WorkflowCancelRunResp{}, nil } func (s *WorkflowService) Shutdown(ctx context.Context) { s.dispatcher.Shutdown(ctx) } func (s *WorkflowService) cleanupHistoryRuns(ctx context.Context) error { settings, err := s.settingsRepo.GetByName(ctx, domain.SettingsNamePersistence) if err != nil { if errors.Is(err, domain.ErrRecordNotFound) { return nil } app.GetLogger().Error("failed to get persistence settings", slog.Any("error", err)) return err } persistenceSettings := settings.Content.AsPersistence() if persistenceSettings.WorkflowRunsRetentionMaxDays != 0 { ret, err := s.workflowRunRepo.DeleteWhere( ctx, dbx.NewExp(fmt.Sprintf("status!='%s'", string(domain.WorkflowRunStatusTypePending))), dbx.NewExp(fmt.Sprintf("status!='%s'", string(domain.WorkflowRunStatusTypeProcessing))), dbx.NewExp(fmt.Sprintf("endedAt 0 { app.GetLogger().Info(fmt.Sprintf("cleanup %d workflow history runs", ret)) } } return nil } ================================================ FILE: internal/workflow/service_deps.go ================================================ package workflow import ( "context" "github.com/pocketbase/dbx" "github.com/certimate-go/certimate/internal/domain" ) type workflowRepository interface { ListEnabledScheduled(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) } type workflowRunRepository interface { GetById(ctx context.Context, id string) (*domain.WorkflowRun, error) Save(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error) SaveWithCascading(ctx context.Context, workflowRun *domain.WorkflowRun) (*domain.WorkflowRun, error) DeleteWhere(ctx context.Context, exprs ...dbx.Expression) (int, error) } type settingsRepository interface { GetByName(ctx context.Context, name string) (*domain.Settings, error) } ================================================ FILE: internal/workflow/service_inst.go ================================================ package workflow import ( "sync" "github.com/certimate-go/certimate/internal/repository" ) var ( thisSvc *WorkflowService thisSvcOnce sync.Once ) func thisSvcInst() *WorkflowService { thisSvcOnce.Do(func() { thisSvc = NewWorkflowService(repository.NewWorkflowRepository(), repository.NewWorkflowRunRepository(), repository.NewSettingsRepository()) }) return thisSvc } ================================================ FILE: internal/workflow/workflow.go ================================================ package workflow import ( "context" ) func Setup() { registerWorkflowRecordEvents() } func Teardown() { thisSvcInst().Shutdown(context.Background()) } ================================================ FILE: main.go ================================================ package main import ( "log/slog" "os" "strings" _ "time/tzdata" "github.com/pocketbase/pocketbase" "github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/plugins/migratecmd" "github.com/pocketbase/pocketbase/tools/hook" "github.com/spf13/pflag" "github.com/certimate-go/certimate/cmd" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/internal/rest/routes" "github.com/certimate-go/certimate/internal/scheduler" "github.com/certimate-go/certimate/internal/workflow" "github.com/certimate-go/certimate/ui" _ "github.com/certimate-go/certimate/migrations" ) func main() { pb := app.GetApp().(*pocketbase.PocketBase) if len(os.Args) < 2 { slog.Error("[CERTIMATE] missing exec args, maybe you forget the 'serve' command?") os.Exit(1) return } var flagHttp string pflag.CommandLine = pflag.NewFlagSet(os.Args[0], pflag.ContinueOnError) pflag.CommandLine.Parse(os.Args[2:]) // skip the first two arguments: "main.go serve" pflag.StringVar(&flagHttp, "http", "127.0.0.1:8090", "HTTP server address") pflag.Parse() migratecmd.MustRegister(pb, pb.RootCmd, migratecmd.Config{ // enable auto creation of migration files when making collection changes in the Admin UI // (the isGoRun check is to enable it only during development) Automigrate: strings.HasPrefix(os.Args[0], os.TempDir()), }) pb.RootCmd.AddCommand(cmd.NewInternalCommand(pb)) pb.RootCmd.AddCommand(cmd.NewWinscCommand(pb)) pb.OnServe().BindFunc(func(e *core.ServeEvent) error { scheduler.Setup() workflow.Setup() routes.BindRouter(e.Router) return e.Next() }) pb.OnServe().Bind(&hook.Handler[*core.ServeEvent]{ Func: func(e *core.ServeEvent) error { e.Router. GET("/{path...}", apis.Static(ui.DistDirFS, false)). Bind(apis.Gzip()) return e.Next() }, Priority: 999, }) pb.OnServe().BindFunc(func(e *core.ServeEvent) error { slog.Info("[CERTIMATE] Visit the website: http://" + flagHttp) return e.Next() }) pb.OnTerminate().BindFunc(func(e *core.TerminateEvent) error { workflow.Teardown() return e.Next() }) if err := cmd.Serve(pb); err != nil { slog.Error("[CERTIMATE] Start failed.", slog.Any("error", err)) } } ================================================ FILE: migrations/1757476800_upgrade_v0.4.0.go ================================================ package migrations import ( "database/sql" "encoding/json" "errors" "fmt" "regexp" "strings" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" "github.com/samber/lo" snapsv03 "github.com/certimate-go/certimate/migrations/snaps/v0.3" snapsv04 "github.com/certimate-go/certimate/migrations/snaps/v0.4" ) func init() { m.Register(func(app core.App) error { if err := app.DB(). NewQuery("SELECT (1) FROM _migrations WHERE file={:file} LIMIT 1"). Bind(dbx.Params{"file": "1757476800_m0.4.0_migrate.go"}). One(&struct{}{}); err == nil { return nil } tracer := NewTracer("v0.4.0") tracer.Printf("go ...") // update collection `settings` // - delete records: 'notifyChannels', 'notifyTemplates' { collection, err := app.FindCollectionByNameOrId("dy6ccjb60spfy6p") if err != nil { if !errors.Is(err, sql.ErrNoRows) { return err } } else { if _, err := app.DB().NewQuery("DELETE FROM settings WHERE name = 'notifyChannels'").Execute(); err != nil { return err } if _, err := app.DB().NewQuery("DELETE FROM settings WHERE name = 'notifyTemplates'").Execute(); err != nil { return err } tracer.Printf("collection '%s' updated", collection.Name) } } // update collection `acme_accounts` // - add field `acmeAcctUrl` // - add field `acmeDirUrl` // - rename field `key` to `privateKey` // - rename field `resource` to `acmeAccount` // - migrate field `acmeAccount` { collection, err := app.FindCollectionByNameOrId("012d7abbod1hwvr") if err != nil { if !errors.Is(err, sql.ErrNoRows) { return err } } else { if err := collection.Fields.AddMarshaledJSONAt(5, []byte(`{ "exceptDomains": null, "hidden": false, "id": "url2424532088", "name": "acmeAcctUrl", "onlyDomains": null, "presentable": false, "required": false, "system": false, "type": "url" }`)); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{ "exceptDomains": null, "hidden": false, "id": "url3632694140", "name": "acmeDirUrl", "onlyDomains": null, "presentable": false, "required": false, "system": false, "type": "url" }`)); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(3, []byte(`{ "autogeneratePattern": "", "hidden": false, "id": "genxqtii", "max": 0, "min": 0, "name": "privateKey", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }`)); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(4, []byte(`{ "hidden": false, "id": "1aoia909", "maxSize": 2000000, "name": "acmeAccount", "presentable": false, "required": false, "system": false, "type": "json" }`)); err != nil { return err } if err := app.Save(collection); err != nil { return err } records, err := app.FindAllRecords(collection) if err != nil { return err } for _, record := range records { changed := false deleted := false resource := make(map[string]any) if err := record.UnmarshalJSONField("acmeAccount", &resource); err != nil { return err } if _, ok := resource["body"]; ok { record.Set("acmeAcctUrl", resource["uri"].(string)) record.Set("acmeAccount", resource["body"].(map[string]any)) changed = true } ca := record.GetString("ca") if strings.Contains(ca, "#") { record.Set("ca", strings.Split(ca, "#")[0]) if access, err := app.FindRecordById("access", strings.Split(ca, "#")[1]); err != nil { deleted = true } else { provider := access.GetString("provider") switch provider { case "buypass": record.Set("acmeDirUrl", "https://api.buypass.com/acme/directory") changed = true case "googletrustservices": record.Set("acmeDirUrl", "https://dv.acme-v02.api.pki.goog/directory") changed = true case "sslcom": record.Set("acmeDirUrl", "https://acme.ssl.com/sslcom-dv-rsa") changed = true case "zerossl": record.Set("acmeDirUrl", "https://acme.zerossl.com/v2/DV90") changed = true case "acmeca": accessConfig := make(map[string]any) access.UnmarshalJSONField("config", &accessConfig) record.Set("acmeDirUrl", accessConfig["endpoint"].(string)) changed = true } } } else { switch ca { case "letsencrypt": record.Set("acmeDirUrl", "https://acme-v02.api.letsencrypt.org/directory") changed = true case "letsencryptstaging": record.Set("acmeDirUrl", "https://acme-staging-v02.api.letsencrypt.org/directory") changed = true case "buypass": record.Set("acmeDirUrl", "https://api.buypass.com/acme/directory") changed = true case "googletrustservices": record.Set("acmeDirUrl", "https://dv.acme-v02.api.pki.goog/directory") changed = true case "sslcom": record.Set("acmeDirUrl", "https://acme.ssl.com/sslcom-dv-rsa") changed = true case "zerossl": record.Set("acmeDirUrl", "https://acme.zerossl.com/v2/DV90") changed = true } } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } if deleted { if err := app.Delete(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' deleted", record.Id, collection.Name) } } tracer.Printf("collection '%s' updated", collection.Name) } } // update collection `access` // - modify field `config` schema: rename property `defaultReceiver` to `receiver` // - modify field `reserve` candidates // - delete records: 'local', 'buypass' { collection, err := app.FindCollectionByNameOrId("4yzbv8urny5ja1e") if err != nil { if !errors.Is(err, sql.ErrNoRows) { return err } } else { if _, err := app.DB().NewQuery("UPDATE access SET reserve = 'notif' WHERE reserve = 'notification'").Execute(); err != nil { return err } if _, err := app.DB().NewQuery("DELETE FROM access WHERE provider = 'local'").Execute(); err != nil { return err } if _, err := app.DB().NewQuery("DELETE FROM access WHERE provider = 'buypass'").Execute(); err != nil { return err } records, err := app.FindAllRecords(collection) if err != nil { return err } for _, record := range records { changed := false provider := record.GetString("provider") config := make(map[string]any) if err := record.UnmarshalJSONField("config", &config); err != nil { return err } switch provider { case "discordbot", "mattermost", "slackbot": if _, ok := config["defaultChannelId"]; ok { config["channelId"] = config["defaultChannelId"] delete(config, "defaultChannelId") record.Set("config", config) changed = true } case "email": if _, ok := config["defaultSenderAddress"]; ok { config["senderAddress"] = config["defaultSenderAddress"] delete(config, "defaultSenderAddress") record.Set("config", config) changed = true } if _, ok := config["defaultSenderName"]; ok { config["senderName"] = config["defaultSenderName"] delete(config, "defaultSenderName") record.Set("config", config) changed = true } if _, ok := config["defaultReceiverAddress"]; ok { config["receiverAddress"] = config["defaultReceiverAddress"] delete(config, "defaultReceiverAddress") record.Set("config", config) changed = true } case "telegrambot": if _, ok := config["defaultChatId"]; ok { config["chatId"] = config["defaultChatId"] delete(config, "defaultChatId") record.Set("config", config) changed = true } case "webhook": if _, ok := config["defaultDataForDeployment"]; ok { if existsData, exists := config["data"]; !exists || existsData == "" { config["data"] = config["defaultDataForDeployment"] delete(config, "defaultDataForDeployment") record.Set("config", config) changed = true } } if _, ok := config["defaultDataForNotification"]; ok { if existsData, exists := config["data"]; !exists || existsData == "" { config["data"] = config["defaultDataForNotification"] delete(config, "defaultDataForNotification") record.Set("config", config) changed = true } } if _, ok := config["dataForDeployment"]; ok { if existsData, exists := config["data"]; !exists || existsData == "" { config["data"] = config["dataForDeployment"] delete(config, "dataForDeployment") record.Set("config", config) changed = true } } if _, ok := config["dataForNotification"]; ok { if existsData, exists := config["data"]; !exists || existsData == "" { config["data"] = config["dataForNotification"] delete(config, "dataForNotification") record.Set("config", config) changed = true } } } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } } } // update collection `certificate` // - modify field `source` candidates // - rename field `effectAt` to `validityNotBefore` // - rename field `expireAt` to `validityNotAfter` // - rename field `acmeAccountUrl` to `acmeAcctUrl` // - rename field `workflowId` to `workflowRef` // - rename field `workflowRunId` to `workflowRunRef` // - rename field `workflowOutputId`(aka `workflowOutputRef`) { collection, err := app.FindCollectionByNameOrId("4szxr9x43tpj6np") if err != nil { if !errors.Is(err, sql.ErrNoRows) { return err } } else { if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{ "hidden": false, "id": "by9hetqi", "maxSelect": 1, "name": "source", "presentable": false, "required": false, "system": false, "type": "select", "values": [ "request", "upload" ] }`)); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(9, []byte(`{ "hidden": false, "id": "v40aqzpd", "max": "", "min": "", "name": "validityNotBefore", "presentable": false, "required": false, "system": false, "type": "date" }`)); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(10, []byte(`{ "hidden": false, "id": "zgpdby2k", "max": "", "min": "", "name": "validityNotAfter", "presentable": false, "required": false, "system": false, "type": "date" }`)); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(11, []byte(`{ "autogeneratePattern": "", "hidden": false, "id": "text2045248758", "max": 0, "min": 0, "name": "acmeAcctUrl", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }`)); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(15, []byte(`{ "cascadeDelete": false, "collectionId": "tovyif5ax6j62ur", "hidden": false, "id": "uvqfamb1", "maxSelect": 1, "minSelect": 0, "name": "workflowRef", "presentable": false, "required": false, "system": false, "type": "relation" }`)); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(16, []byte(`{ "cascadeDelete": false, "collectionId": "qjp8lygssgwyqyz", "hidden": false, "id": "relation3917999135", "maxSelect": 1, "minSelect": 0, "name": "workflowRunRef", "presentable": false, "required": false, "system": false, "type": "relation" }`)); err != nil { return err } collection.Fields.RemoveByName("workflowOutputId") collection.Fields.RemoveByName("workflowOutputRef") if err := json.Unmarshal([]byte(`{ "indexes": [ "CREATE INDEX `+"`"+`idx_Jx8TXzDCmw`+"`"+` ON `+"`"+`certificate`+"`"+` (`+"`"+`workflowRef`+"`"+`)", "CREATE INDEX `+"`"+`idx_2cRXqNDyyp`+"`"+` ON `+"`"+`certificate`+"`"+` (`+"`"+`workflowRunRef`+"`"+`)", "CREATE INDEX `+"`"+`idx_kcKpgAZapk`+"`"+` ON `+"`"+`certificate`+"`"+` (`+"`"+`workflowNodeId`+"`"+`)" ] }`), &collection); err != nil { return err } if err := app.Save(collection); err != nil { return err } if _, err := app.DB().NewQuery("UPDATE certificate SET source = 'request' WHERE source = 'workflow'").Execute(); err != nil { return err } tracer.Printf("collection '%s' updated", collection.Name) } } // update collection `workflow` // - modify field `trigger` candidates, and cascading migrate field `graphDraft` / `graphContent` // - modify field `lastRunStatus` candidates // - rename field `lastRunRefId` to `lastRunRef` // - rename field `draft` to `graphDraft` // - rename field `content` to `graphContent` // - add field `hasContent` { collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur") if err != nil { if !errors.Is(err, sql.ErrNoRows) { return err } } else { if err := collection.Fields.AddMarshaledJSONAt(3, []byte(`{ "hidden": false, "id": "vqoajwjq", "maxSelect": 1, "name": "trigger", "presentable": false, "required": false, "system": false, "type": "select", "values": [ "manual", "scheduled" ] }`)); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{ "hidden": false, "id": "g9ohkk5o", "maxSize": 5000000, "name": "graphDraft", "presentable": false, "required": false, "system": false, "type": "json" }`)); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(7, []byte(`{ "hidden": false, "id": "awlphkfe", "maxSize": 5000000, "name": "graphContent", "presentable": false, "required": false, "system": false, "type": "json" }`)); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(9, []byte(`{ "hidden": false, "id": "bool3832150317", "name": "hasContent", "presentable": false, "required": false, "system": false, "type": "bool" }`)); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(10, []byte(`{ "cascadeDelete": false, "collectionId": "qjp8lygssgwyqyz", "hidden": false, "id": "a23wkj9x", "maxSelect": 1, "minSelect": 0, "name": "lastRunRef", "presentable": false, "required": false, "system": false, "type": "relation" }`)); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(11, []byte(`{ "hidden": false, "id": "zivdxh23", "maxSelect": 1, "name": "lastRunStatus", "presentable": false, "required": false, "system": false, "type": "select", "values": [ "pending", "processing", "succeeded", "failed", "canceled" ] }`)); err != nil { return err } if err := app.Save(collection); err != nil { return err } if _, err := app.DB().NewQuery("UPDATE workflow SET trigger = 'scheduled' WHERE trigger = 'auto'").Execute(); err != nil { return err } if _, err := app.DB().NewQuery("UPDATE workflow SET hasContent = TRUE WHERE graphContent IS NOT NULL").Execute(); err != nil { return err } if _, err := app.DB().NewQuery("UPDATE workflow SET lastRunStatus = 'processing' WHERE lastRunStatus = 'running'").Execute(); err != nil { return err } tracer.Printf("collection '%s' updated", collection.Name) records, err := app.FindAllRecords(collection) if err != nil { return err } else { for _, record := range records { changed := false graphDraft := make(map[string]any) if err := record.UnmarshalJSONField("graphDraft", &graphDraft); err == nil { if _, ok := graphDraft["config"]; ok { config := graphDraft["config"].(map[string]any) if _, ok := config["trigger"]; ok { trigger := config["trigger"].(string) if trigger == "auto" { config["trigger"] = "scheduled" record.Set("graphDraft", graphDraft) changed = true } } } } graphContent := make(map[string]any) if err := record.UnmarshalJSONField("graphContent", &graphContent); err == nil { if _, ok := graphContent["config"]; ok { config := graphContent["config"].(map[string]any) if _, ok := config["trigger"]; ok { trigger := config["trigger"].(string) if trigger == "auto" { config["trigger"] = "scheduled" record.Set("graphContent", graphContent) changed = true } } } } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } } } } // update collection `workflow_run` // - modify field `trigger` candidates, and cascading migrate field `graph` // - modify field `status` candidates // - rename field `detail` to `graph` // - rename field `workflowId` to `workflowRef` { collection, err := app.FindCollectionByNameOrId("qjp8lygssgwyqyz") if err != nil { if !errors.Is(err, sql.ErrNoRows) { return err } } else { if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{ "cascadeDelete": true, "collectionId": "tovyif5ax6j62ur", "hidden": false, "id": "m8xfsyyy", "maxSelect": 1, "minSelect": 0, "name": "workflowRef", "presentable": false, "required": false, "system": false, "type": "relation" }`)); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{ "hidden": false, "id": "qldmh0tw", "maxSelect": 1, "name": "status", "presentable": false, "required": false, "system": false, "type": "select", "values": [ "pending", "processing", "succeeded", "failed", "canceled" ] }`)); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(3, []byte(`{ "hidden": false, "id": "jlroa3fk", "maxSelect": 1, "name": "trigger", "presentable": false, "required": false, "system": false, "type": "select", "values": [ "manual", "scheduled" ] }`)); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{ "hidden": false, "id": "json772177811", "maxSize": 5000000, "name": "graph", "presentable": false, "required": false, "system": false, "type": "json" }`)); err != nil { return err } if err := json.Unmarshal([]byte(`{ "indexes": [ "CREATE INDEX `+"`"+`idx_7ZpfjTFsD2`+"`"+` ON `+"`"+`workflow_run`+"`"+` (`+"`"+`workflowRef`+"`"+`)" ] }`), &collection); err != nil { return err } if err := app.Save(collection); err != nil { return err } if _, err := app.DB().NewQuery("UPDATE workflow_run SET trigger = 'scheduled' WHERE trigger = 'auto'").Execute(); err != nil { return err } if _, err := app.DB().NewQuery("UPDATE workflow_run SET status = 'processing' WHERE status = 'running'").Execute(); err != nil { return err } tracer.Printf("collection '%s' updated", collection.Name) records, err := app.FindAllRecords(collection) if err != nil { return err } else { for _, record := range records { changed := false graphContent := make(map[string]any) if err := record.UnmarshalJSONField("graph", &graphContent); err == nil { if _, ok := graphContent["config"]; ok { config := graphContent["config"].(map[string]any) if _, ok := config["trigger"]; ok { trigger := config["trigger"].(string) if trigger == "auto" { config["trigger"] = "scheduled" record.Set("graph", graphContent) changed = true } } } } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } } } } // update collection `workflow_output` // - rename field `workflowId` to `workflowRef` // - rename field `runId` to `runRef` // - rename field `node` to `nodeConfig` { collection, err := app.FindCollectionByNameOrId("bqnxb95f2cooowp") if err != nil { if !errors.Is(err, sql.ErrNoRows) { return err } } else { if err := json.Unmarshal([]byte(`{ "indexes": [ "CREATE INDEX `+"`"+`idx_BYoQPsz4my`+"`"+` ON `+"`"+`workflow_output`+"`"+` (`+"`"+`workflowRef`+"`"+`)", "CREATE INDEX `+"`"+`idx_O9zxLETuxJ`+"`"+` ON `+"`"+`workflow_output`+"`"+` (`+"`"+`runRef`+"`"+`)", "CREATE INDEX `+"`"+`idx_luac8Ul34G`+"`"+` ON `+"`"+`workflow_output`+"`"+` (`+"`"+`nodeId`+"`"+`)" ] }`), &collection); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{ "cascadeDelete": true, "collectionId": "tovyif5ax6j62ur", "hidden": false, "id": "jka88auc", "maxSelect": 1, "minSelect": 0, "name": "workflowRef", "presentable": false, "required": false, "system": false, "type": "relation" }`)); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{ "cascadeDelete": true, "collectionId": "qjp8lygssgwyqyz", "hidden": false, "id": "relation821863227", "maxSelect": 1, "minSelect": 0, "name": "runRef", "presentable": false, "required": false, "system": false, "type": "relation" }`)); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(4, []byte(`{ "hidden": false, "id": "json2239752261", "maxSize": 5000000, "name": "nodeConfig", "presentable": false, "required": false, "system": false, "type": "json" }`)); err != nil { return err } if err := app.Save(collection); err != nil { return err } tracer.Printf("collection '%s' updated", collection.Name) } } // update collection `workflow_logs` // - modify field `level` type // - rename field `workflowId` to `workflowRef` // - rename field `runId` to `runRef` // - migrate field `message` { collection, err := app.FindCollectionByNameOrId("pbc_1682296116") if err != nil { if !errors.Is(err, sql.ErrNoRows) { return err } } else { if field := collection.Fields.GetByName("level"); field != nil && field.Type() == "text" { if _, err := app.DB().NewQuery("UPDATE workflow_logs SET level = '-4' WHERE level = 'DEBUG'").Execute(); err != nil { return err } if _, err := app.DB().NewQuery("UPDATE workflow_logs SET level = '0' WHERE level = 'INFO'").Execute(); err != nil { return err } if _, err := app.DB().NewQuery("UPDATE workflow_logs SET level = '4' WHERE level = 'WARN'").Execute(); err != nil { return err } if _, err := app.DB().NewQuery("UPDATE workflow_logs SET level = '8' WHERE level = 'ERROR'").Execute(); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(7, []byte(`{ "hidden": false, "id": "number760395071", "max": null, "min": null, "name": "levelTmp", "onlyInt": false, "presentable": false, "required": false, "system": false, "type": "number" }`)); err != nil { return err } if err := app.Save(collection); err != nil { return err } if _, err := app.DB().NewQuery("UPDATE workflow_logs SET levelTmp = level").Execute(); err != nil { return err } collection.Fields.RemoveById(field.GetId()) if err := app.Save(collection); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{ "hidden": false, "id": "number760395071", "max": null, "min": null, "name": "level", "onlyInt": false, "presentable": false, "required": false, "system": false, "type": "number" }`)); err != nil { return err } if err := app.Save(collection); err != nil { return err } } if err := collection.Fields.AddMarshaledJSONAt(1, []byte(`{ "cascadeDelete": true, "collectionId": "tovyif5ax6j62ur", "hidden": false, "id": "relation3371272342", "maxSelect": 1, "minSelect": 0, "name": "workflowRef", "presentable": false, "required": false, "system": false, "type": "relation" }`)); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{ "cascadeDelete": true, "collectionId": "qjp8lygssgwyqyz", "hidden": false, "id": "relation821863227", "maxSelect": 1, "minSelect": 0, "name": "runRef", "presentable": false, "required": false, "system": false, "type": "relation" }`)); err != nil { return err } if err := json.Unmarshal([]byte(`{ "indexes": [ "CREATE INDEX `+"`"+`idx_IOlpy6XuJ2`+"`"+` ON `+"`"+`workflow_logs`+"`"+` (`+"`"+`workflowRef`+"`"+`)", "CREATE INDEX `+"`"+`idx_qVlTb2yl7v`+"`"+` ON `+"`"+`workflow_logs`+"`"+` (`+"`"+`runRef`+"`"+`)", "CREATE INDEX `+"`"+`idx_UL4tdCXNlA`+"`"+` ON `+"`"+`workflow_logs`+"`"+` (`+"`"+`nodeId`+"`"+`)" ] }`), &collection); err != nil { return err } if _, err := app.DB().NewQuery("UPDATE workflow_logs SET message = REPLACE(message, 'certificiate', 'certificate') WHERE level = 0").Execute(); err != nil { return err } if _, err := app.DB().NewQuery("UPDATE workflow_logs SET message = REPLACE(message, 'ready to apply certificate', 'ready to request certificate') WHERE level = 0").Execute(); err != nil { return err } if _, err := app.DB().NewQuery("UPDATE workflow_logs SET message = REPLACE(message, 'ready to obtain certificate', 'ready to request certificate') WHERE level = 0").Execute(); err != nil { return err } if err := app.Save(collection); err != nil { return err } tracer.Printf("collection '%s' updated", collection.Name) } } // adapt to new workflow data structure { convertNode := func(root *snapsv03.WorkflowNode) []*snapsv04.WorkflowNode { lang := lo. IfF(root == nil, func() string { return "zh" }). ElseIf(regexp.MustCompile(`[\p{Han}]`).MatchString(root.Name), "zh"). Else("en") var deepConvertNode func(node *snapsv03.WorkflowNode) []*snapsv04.WorkflowNode deepConvertNode = func(node *snapsv03.WorkflowNode) []*snapsv04.WorkflowNode { temp := make([]*snapsv04.WorkflowNode, 0) current := node for current != nil { current.Config = lo.PickBy(current.Config, func(key string, value any) bool { str, ok := value.(string) return !ok || str != "" }) switch current.Type { case "start": temp = append(temp, &snapsv04.WorkflowNode{ Id: current.Id, Type: "start", Data: snapsv04.WorkflowNodeData{ Name: current.Name, Config: current.Config, }, }) case "apply": if _, ok := current.Config["challengeType"].(string); !ok { current.Config["challengeType"] = "dns-01" } temp = append(temp, &snapsv04.WorkflowNode{ Id: current.Id, Type: "bizApply", Data: snapsv04.WorkflowNodeData{ Name: current.Name, Config: current.Config, }, }) case "upload": if _, ok := current.Config["source"].(string); !ok { current.Config["source"] = "form" } temp = append(temp, &snapsv04.WorkflowNode{ Id: current.Id, Type: "bizUpload", Data: snapsv04.WorkflowNodeData{ Name: current.Name, Config: current.Config, }, }) case "monitor": temp = append(temp, &snapsv04.WorkflowNode{ Id: current.Id, Type: "bizMonitor", Data: snapsv04.WorkflowNodeData{ Name: current.Name, Config: current.Config, }, }) case "deploy": if s, ok := current.Config["certificate"].(string); ok { current.Config["certificateOutputNodeId"] = strings.Split(s, "#")[0] delete(current.Config, "certificate") } temp = append(temp, &snapsv04.WorkflowNode{ Id: current.Id, Type: "bizDeploy", Data: snapsv04.WorkflowNodeData{ Name: current.Name, Config: current.Config, }, }) case "notify": if _, ok := current.Config["channel"].(string); ok { delete(current.Config, "channel") } temp = append(temp, &snapsv04.WorkflowNode{ Id: current.Id, Type: "bizNotify", Data: snapsv04.WorkflowNodeData{ Name: current.Name, Config: current.Config, }, }) case "execute_result_branch": if len(temp) == 0 { break } tryNode, _ := lo.Last(temp) temp = lo.DropRight(temp, 1) branches := lo.GroupBy(current.Branches, func(b *snapsv03.WorkflowNode) string { return b.Type }) successBranch := lo.IfF(len(branches["execute_success"]) > 0, func() *snapsv03.WorkflowNode { return branches["execute_success"][0] }).Else(nil) failureBranch := lo.IfF(len(branches["execute_failure"]) > 0, func() *snapsv03.WorkflowNode { return branches["execute_failure"][0] }).Else(nil) successBranchId := lo.If(successBranch != nil, successBranch.Id).Else(core.GenerateDefaultRandomId()) failureBranchId := lo.If(failureBranch != nil, failureBranch.Id).Else(core.GenerateDefaultRandomId()) catchBlocks := lo.If(failureBranch != nil && failureBranch.Next != nil, deepConvertNode(failureBranch.Next)).Else([]*snapsv04.WorkflowNode{}) catchBlocks = append(catchBlocks, &snapsv04.WorkflowNode{ Id: core.GenerateDefaultRandomId(), Type: "end", Data: snapsv04.WorkflowNodeData{ Name: lo.If(lang == "en", "End").Else("结束"), }, }) tryCatchNode := &snapsv04.WorkflowNode{ Id: current.Id, Type: "tryCatch", Data: snapsv04.WorkflowNodeData{ Name: lo.If(lang == "en", "Try to ...").Else("尝试执行…"), Config: current.Config, }, Blocks: []*snapsv04.WorkflowNode{ { Id: successBranchId, Type: "tryBlock", Data: snapsv04.WorkflowNodeData{ Name: "", }, Blocks: []*snapsv04.WorkflowNode{tryNode}, }, { Id: failureBranchId, Type: "catchBlock", Data: snapsv04.WorkflowNodeData{ Name: lo.If(lang == "en", "On failed ...").Else("若执行失败…"), }, Blocks: catchBlocks, }, }, } temp = append(temp, tryCatchNode) current = successBranch case "branch": branchNode := &snapsv04.WorkflowNode{ Id: current.Id, Type: "condition", Data: snapsv04.WorkflowNodeData{ Name: lo.If(lang == "en", "Parallel").Else("并行"), Config: current.Config, }, Blocks: lo.Map(current.Branches, func(b *snapsv03.WorkflowNode, _ int) *snapsv04.WorkflowNode { return &snapsv04.WorkflowNode{ Id: b.Id, Type: "branchBlock", Data: snapsv04.WorkflowNodeData{ Name: b.Name, Config: b.Config, }, Blocks: deepConvertNode(b.Next), } }), } temp = append(temp, branchNode) } if current != nil { current = current.Next } } return temp } nodes := lo.Ternary(root == nil, []*snapsv04.WorkflowNode{ { Id: core.GenerateDefaultRandomId(), Type: "start", Data: snapsv04.WorkflowNodeData{ Name: lo.If(lang == "en", "Start").Else("开始"), }, }, }, deepConvertNode(root)) return append(nodes, &snapsv04.WorkflowNode{ Id: core.GenerateDefaultRandomId(), Type: "end", Data: snapsv04.WorkflowNodeData{ Name: lo.If(lang == "en", "End").Else("结束"), }, }) } // update collection `workflow` // - migrate field `graphDraft` / `graphContent` { collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur") if err != nil { if !errors.Is(err, sql.ErrNoRows) { return err } } else { records, err := app.FindAllRecords(collection) if err != nil { return err } else { for _, record := range records { changed := false graphDraft := make(map[string]any) if err := record.UnmarshalJSONField("graphDraft", &graphDraft); err == nil { if len(graphDraft) > 0 { if _, ok := graphDraft["nodes"]; !ok { legacyRootNode := &snapsv03.WorkflowNode{} if err := record.UnmarshalJSONField("graphDraft", legacyRootNode); err != nil { return err } else { graphDraft = make(map[string]any) graphDraft["nodes"] = convertNode(legacyRootNode) record.Set("graphDraft", graphDraft) changed = true } } } } graphContent := make(map[string]any) if err := record.UnmarshalJSONField("graphContent", &graphContent); err == nil { if len(graphContent) > 0 { if _, ok := graphContent["nodes"]; !ok { legacyRootNode := &snapsv03.WorkflowNode{} if err := record.UnmarshalJSONField("graphContent", legacyRootNode); err != nil { return err } else { graphContent = make(map[string]any) graphContent["nodes"] = convertNode(legacyRootNode) record.Set("graphContent", graphContent) record.Set("hasContent", true) changed = true } } } } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } } } } // update collection `workflow_run` // - migrate field `graph` { collection, err := app.FindCollectionByNameOrId("qjp8lygssgwyqyz") if err != nil { if !errors.Is(err, sql.ErrNoRows) { return err } } else { records, err := app.FindAllRecords(collection) if err != nil { return err } else { for _, record := range records { changed := false graphContent := make(map[string]any) if err := record.UnmarshalJSONField("graph", &graphContent); err == nil { if len(graphContent) > 0 { if _, ok := graphContent["nodes"]; !ok { legacyRootNode := &snapsv03.WorkflowNode{} if err := record.UnmarshalJSONField("graph", legacyRootNode); err != nil { return err } else { graphContent = make(map[string]any) graphContent["nodes"] = convertNode(legacyRootNode) record.Set("graph", graphContent) changed = true } } } } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } } } } // update collection `workflow_output` // - migrate field `nodeConfig` // - migrate field `outputs` { collection, err := app.FindCollectionByNameOrId("bqnxb95f2cooowp") if err != nil { if !errors.Is(err, sql.ErrNoRows) { return err } } else { records, err := app.FindAllRecords(collection) if err != nil { return err } else { for _, record := range records { changed := false nodeConfig := make(map[string]any) if err := record.UnmarshalJSONField("nodeConfig", &nodeConfig); err == nil { if _, ok := nodeConfig["id"]; ok { if _, ok := nodeConfig["type"]; ok { if _, ok := nodeConfig["config"]; ok { record.Set("nodeConfig", nodeConfig["config"]) changed = true } } } } outputs := make([]map[string]any, 0) if err := record.UnmarshalJSONField("outputs", &outputs); err == nil { for i, output := range outputs { if _, ok := output["label"]; ok { output["valueType"] = "string" delete(output, "label") delete(output, "required") delete(output, "valueSelector") if output["type"] == "certificate" { output["type"] = "ref" output["value"] = fmt.Sprintf("certificate#%s", output["value"]) } outputs[i] = output } else { continue } record.Set("outputs", outputs) changed = true } } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } } } } // normalize field `nodeId` in collection `workflow`, `workflow_run`, `workflow_output`, `workflow_logs` const ATTEMPTS = 3 for i := 1; i <= ATTEMPTS; i++ { app.DB().NewQuery(`UPDATE workflow SET graphDraft=REPLACE(graphDraft, '"id":"-', '"id":"')`).Execute() app.DB().NewQuery(`UPDATE workflow SET graphDraft=REPLACE(graphDraft, '"id":"_', '"id":"')`).Execute() app.DB().NewQuery(`UPDATE workflow SET graphContent=REPLACE(graphContent, '"id":"-', '"id":"')`).Execute() app.DB().NewQuery(`UPDATE workflow SET graphContent=REPLACE(graphContent, '"id":"_', '"id":"')`).Execute() app.DB().NewQuery(`UPDATE workflow_run SET graph=REPLACE(graph, '"id":"-', '"id":"')`).Execute() app.DB().NewQuery(`UPDATE workflow_run SET graph=REPLACE(graph, '"id":"_', '"id":"')`).Execute() app.DB().NewQuery(`UPDATE workflow_output SET nodeId=SUBSTR(nodeId, 2) WHERE nodeId LIKE '-%'`).Execute() app.DB().NewQuery(`UPDATE workflow_output SET nodeId=SUBSTR(nodeId, 2) WHERE nodeId LIKE '\_%' ESCAPE '\'`).Execute() app.DB().NewQuery(`UPDATE workflow_logs SET nodeId=SUBSTR(nodeId, 2) WHERE nodeId LIKE '-%'`).Execute() app.DB().NewQuery(`UPDATE workflow_logs SET nodeId=SUBSTR(nodeId, 2) WHERE nodeId LIKE '\_%' ESCAPE '\'`).Execute() } } tracer.Printf("done") return nil }, func(app core.App) error { return nil }) } ================================================ FILE: migrations/1757476801_initialize_v0.4.0.go ================================================ package migrations import ( "os" "strings" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" ) func init() { m.Register(func(app core.App) error { if err := app.DB(). NewQuery("SELECT (1) FROM _migrations WHERE file={:file} LIMIT 1"). Bind(dbx.Params{"file": "1757476801_m0.4.0_initialize.go"}). One(&struct{}{}); err == nil { return nil } // snapshot { jsonData := `[ { "fields": [ { "autogeneratePattern": "[a-z0-9]{15}", "hidden": false, "id": "text3208210256", "max": 15, "min": 15, "name": "id", "pattern": "^[a-z0-9]+$", "presentable": false, "primaryKey": true, "required": true, "system": true, "type": "text" }, { "autogeneratePattern": "", "hidden": false, "id": "geeur58v", "max": 0, "min": 0, "name": "name", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "autogeneratePattern": "", "hidden": false, "id": "text2024822322", "max": 0, "min": 0, "name": "provider", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "hidden": false, "id": "iql7jpwx", "maxSize": 2000000, "name": "config", "presentable": false, "required": false, "system": false, "type": "json" }, { "autogeneratePattern": "", "hidden": false, "id": "text2859962647", "max": 0, "min": 0, "name": "reserve", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "hidden": false, "id": "lr33hiwg", "max": "", "min": "", "name": "deleted", "presentable": false, "required": false, "system": false, "type": "date" }, { "hidden": false, "id": "autodate2990389176", "name": "created", "onCreate": true, "onUpdate": false, "presentable": false, "system": false, "type": "autodate" }, { "hidden": false, "id": "autodate3332085495", "name": "updated", "onCreate": true, "onUpdate": true, "presentable": false, "system": false, "type": "autodate" } ], "id": "4yzbv8urny5ja1e", "indexes": [ "CREATE INDEX ` + "`" + `idx_wkoST0j` + "`" + ` ON ` + "`" + `access` + "`" + ` (` + "`" + `name` + "`" + `)", "CREATE INDEX ` + "`" + `idx_frh0JT1Aqx` + "`" + ` ON ` + "`" + `access` + "`" + ` (` + "`" + `provider` + "`" + `)" ], "name": "access", "system": false, "type": "base" }, { "fields": [ { "autogeneratePattern": "[a-z0-9]{15}", "hidden": false, "id": "text3208210256", "max": 15, "min": 15, "name": "id", "pattern": "^[a-z0-9]+$", "presentable": false, "primaryKey": true, "required": true, "system": true, "type": "text" }, { "autogeneratePattern": "", "hidden": false, "id": "1tcmdsdf", "max": 0, "min": 0, "name": "name", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "hidden": false, "id": "f9wyhypi", "maxSize": 2000000, "name": "content", "presentable": false, "required": false, "system": false, "type": "json" }, { "hidden": false, "id": "autodate2990389176", "name": "created", "onCreate": true, "onUpdate": false, "presentable": false, "system": false, "type": "autodate" }, { "hidden": false, "id": "autodate3332085495", "name": "updated", "onCreate": true, "onUpdate": true, "presentable": false, "system": false, "type": "autodate" } ], "id": "dy6ccjb60spfy6p", "indexes": [ "CREATE UNIQUE INDEX ` + "`" + `idx_RO7X9Vw` + "`" + ` ON ` + "`" + `settings` + "`" + ` (` + "`" + `name` + "`" + `)" ], "name": "settings", "system": false, "type": "base" }, { "fields": [ { "autogeneratePattern": "[a-z0-9]{15}", "hidden": false, "id": "text3208210256", "max": 15, "min": 15, "name": "id", "pattern": "^[a-z0-9]+$", "presentable": false, "primaryKey": true, "required": true, "system": true, "type": "text" }, { "autogeneratePattern": "", "hidden": false, "id": "fmjfn0yw", "max": 0, "min": 0, "name": "ca", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "exceptDomains": null, "hidden": false, "id": "qqwijqzt", "name": "email", "onlyDomains": null, "presentable": false, "required": false, "system": false, "type": "email" }, { "autogeneratePattern": "", "hidden": false, "id": "genxqtii", "max": 0, "min": 0, "name": "privateKey", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "hidden": false, "id": "1aoia909", "maxSize": 2000000, "name": "acmeAccount", "presentable": false, "required": false, "system": false, "type": "json" }, { "exceptDomains": null, "hidden": false, "id": "url2424532088", "name": "acmeAcctUrl", "onlyDomains": null, "presentable": false, "required": false, "system": false, "type": "url" }, { "exceptDomains": null, "hidden": false, "id": "url3632694140", "name": "acmeDirUrl", "onlyDomains": null, "presentable": false, "required": false, "system": false, "type": "url" }, { "hidden": false, "id": "autodate2990389176", "name": "created", "onCreate": true, "onUpdate": false, "presentable": false, "system": false, "type": "autodate" }, { "hidden": false, "id": "autodate3332085495", "name": "updated", "onCreate": true, "onUpdate": true, "presentable": false, "system": false, "type": "autodate" } ], "id": "012d7abbod1hwvr", "indexes": [ "CREATE INDEX ` + "`" + `idx_dQiYzimY7m` + "`" + ` ON ` + "`" + `acme_accounts` + "`" + ` (` + "`" + `ca` + "`" + `)", "CREATE INDEX ` + "`" + `idx_TjyqY6LAGa` + "`" + ` ON ` + "`" + `acme_accounts` + "`" + ` (\n ` + "`" + `ca` + "`" + `,\n ` + "`" + `acmeDirUrl` + "`" + `\n)", "CREATE UNIQUE INDEX ` + "`" + `idx_G4brUDgxzc` + "`" + ` ON ` + "`" + `acme_accounts` + "`" + ` (\n ` + "`" + `ca` + "`" + `,\n ` + "`" + `acmeDirUrl` + "`" + `,\n ` + "`" + `acmeAcctUrl` + "`" + `\n)" ], "name": "acme_accounts", "system": false, "type": "base" }, { "fields": [ { "autogeneratePattern": "[a-z0-9]{15}", "hidden": false, "id": "text3208210256", "max": 15, "min": 15, "name": "id", "pattern": "^[a-z0-9]+$", "presentable": false, "primaryKey": true, "required": true, "system": true, "type": "text" }, { "autogeneratePattern": "", "hidden": false, "id": "8yydhv1h", "max": 0, "min": 0, "name": "name", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "autogeneratePattern": "", "hidden": false, "id": "1buzebwz", "max": 0, "min": 0, "name": "description", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "hidden": false, "id": "vqoajwjq", "maxSelect": 1, "name": "trigger", "presentable": false, "required": false, "system": false, "type": "select", "values": [ "manual", "scheduled" ] }, { "autogeneratePattern": "", "hidden": false, "id": "8ho247wh", "max": 0, "min": 0, "name": "triggerCron", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "hidden": false, "id": "nq7kfdzi", "name": "enabled", "presentable": false, "required": false, "system": false, "type": "bool" }, { "hidden": false, "id": "g9ohkk5o", "maxSize": 5000000, "name": "graphDraft", "presentable": false, "required": false, "system": false, "type": "json" }, { "hidden": false, "id": "awlphkfe", "maxSize": 5000000, "name": "graphContent", "presentable": false, "required": false, "system": false, "type": "json" }, { "hidden": false, "id": "2rpfz9t3", "name": "hasDraft", "presentable": false, "required": false, "system": false, "type": "bool" }, { "hidden": false, "id": "bool3832150317", "name": "hasContent", "presentable": false, "required": false, "system": false, "type": "bool" }, { "cascadeDelete": false, "collectionId": "qjp8lygssgwyqyz", "hidden": false, "id": "a23wkj9x", "maxSelect": 1, "minSelect": 0, "name": "lastRunRef", "presentable": false, "required": false, "system": false, "type": "relation" }, { "hidden": false, "id": "zivdxh23", "maxSelect": 1, "name": "lastRunStatus", "presentable": false, "required": false, "system": false, "type": "select", "values": [ "pending", "processing", "succeeded", "failed", "canceled" ] }, { "hidden": false, "id": "u9bosu36", "max": "", "min": "", "name": "lastRunTime", "presentable": false, "required": false, "system": false, "type": "date" }, { "hidden": false, "id": "autodate2990389176", "name": "created", "onCreate": true, "onUpdate": false, "presentable": false, "system": false, "type": "autodate" }, { "hidden": false, "id": "autodate3332085495", "name": "updated", "onCreate": true, "onUpdate": true, "presentable": false, "system": false, "type": "autodate" } ], "id": "tovyif5ax6j62ur", "indexes": [], "name": "workflow", "system": false, "type": "base" }, { "fields": [ { "autogeneratePattern": "[a-z0-9]{15}", "hidden": false, "id": "text3208210256", "max": 15, "min": 15, "name": "id", "pattern": "^[a-z0-9]+$", "presentable": false, "primaryKey": true, "required": true, "system": true, "type": "text" }, { "cascadeDelete": true, "collectionId": "tovyif5ax6j62ur", "hidden": false, "id": "jka88auc", "maxSelect": 1, "minSelect": 0, "name": "workflowRef", "presentable": false, "required": false, "system": false, "type": "relation" }, { "cascadeDelete": true, "collectionId": "qjp8lygssgwyqyz", "hidden": false, "id": "relation821863227", "maxSelect": 1, "minSelect": 0, "name": "runRef", "presentable": false, "required": false, "system": false, "type": "relation" }, { "autogeneratePattern": "", "hidden": false, "id": "z9fgvqkz", "max": 0, "min": 0, "name": "nodeId", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "hidden": false, "id": "json2239752261", "maxSize": 5000000, "name": "nodeConfig", "presentable": false, "required": false, "system": false, "type": "json" }, { "hidden": false, "id": "he4cceqb", "maxSize": 5000000, "name": "outputs", "presentable": false, "required": false, "system": false, "type": "json" }, { "hidden": false, "id": "2yfxbxuf", "name": "succeeded", "presentable": false, "required": false, "system": false, "type": "bool" }, { "hidden": false, "id": "autodate2990389176", "name": "created", "onCreate": true, "onUpdate": false, "presentable": false, "system": false, "type": "autodate" }, { "hidden": false, "id": "autodate3332085495", "name": "updated", "onCreate": true, "onUpdate": true, "presentable": false, "system": false, "type": "autodate" } ], "id": "bqnxb95f2cooowp", "indexes": [ "CREATE INDEX ` + "`" + `idx_BYoQPsz4my` + "`" + ` ON ` + "`" + `workflow_output` + "`" + ` (` + "`" + `workflowRef` + "`" + `)", "CREATE INDEX ` + "`" + `idx_O9zxLETuxJ` + "`" + ` ON ` + "`" + `workflow_output` + "`" + ` (` + "`" + `runRef` + "`" + `)", "CREATE INDEX ` + "`" + `idx_luac8Ul34G` + "`" + ` ON ` + "`" + `workflow_output` + "`" + ` (` + "`" + `nodeId` + "`" + `)" ], "name": "workflow_output", "system": false, "type": "base" }, { "fields": [ { "autogeneratePattern": "[a-z0-9]{15}", "hidden": false, "id": "text3208210256", "max": 15, "min": 15, "name": "id", "pattern": "^[a-z0-9]+$", "presentable": false, "primaryKey": true, "required": true, "system": true, "type": "text" }, { "hidden": false, "id": "by9hetqi", "maxSelect": 1, "name": "source", "presentable": false, "required": false, "system": false, "type": "select", "values": [ "request", "upload" ] }, { "autogeneratePattern": "", "hidden": false, "id": "fugxf58p", "max": 0, "min": 0, "name": "subjectAltNames", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "autogeneratePattern": "", "hidden": false, "id": "text2069360702", "max": 0, "min": 0, "name": "serialNumber", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "autogeneratePattern": "", "hidden": false, "id": "plmambpz", "max": 100000, "min": 0, "name": "certificate", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "autogeneratePattern": "", "hidden": false, "id": "49qvwxcg", "max": 100000, "min": 0, "name": "privateKey", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "autogeneratePattern": "", "hidden": false, "id": "text2910474005", "max": 0, "min": 0, "name": "issuerOrg", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "autogeneratePattern": "", "hidden": false, "id": "agt7n5bb", "max": 100000, "min": 0, "name": "issuerCertificate", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "autogeneratePattern": "", "hidden": false, "id": "text4164403445", "max": 0, "min": 0, "name": "keyAlgorithm", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "hidden": false, "id": "v40aqzpd", "max": "", "min": "", "name": "validityNotBefore", "presentable": false, "required": false, "system": false, "type": "date" }, { "hidden": false, "id": "zgpdby2k", "max": "", "min": "", "name": "validityNotAfter", "presentable": false, "required": false, "system": false, "type": "date" }, { "autogeneratePattern": "", "hidden": false, "id": "text2045248758", "max": 0, "min": 0, "name": "acmeAcctUrl", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "exceptDomains": null, "hidden": false, "id": "ayyjy5ve", "name": "acmeCertUrl", "onlyDomains": null, "presentable": false, "required": false, "system": false, "type": "url" }, { "exceptDomains": null, "hidden": false, "id": "3x5heo8e", "name": "acmeCertStableUrl", "onlyDomains": null, "presentable": false, "required": false, "system": false, "type": "url" }, { "hidden": false, "id": "bool810050391", "name": "acmeRenewed", "presentable": false, "required": false, "system": false, "type": "bool" }, { "cascadeDelete": false, "collectionId": "tovyif5ax6j62ur", "hidden": false, "id": "uvqfamb1", "maxSelect": 1, "minSelect": 0, "name": "workflowRef", "presentable": false, "required": false, "system": false, "type": "relation" }, { "cascadeDelete": false, "collectionId": "qjp8lygssgwyqyz", "hidden": false, "id": "relation3917999135", "maxSelect": 1, "minSelect": 0, "name": "workflowRunRef", "presentable": false, "required": false, "system": false, "type": "relation" }, { "autogeneratePattern": "", "hidden": false, "id": "uqldzldw", "max": 0, "min": 0, "name": "workflowNodeId", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "hidden": false, "id": "klyf4nlq", "max": "", "min": "", "name": "deleted", "presentable": false, "required": false, "system": false, "type": "date" }, { "hidden": false, "id": "autodate2990389176", "name": "created", "onCreate": true, "onUpdate": false, "presentable": false, "system": false, "type": "autodate" }, { "hidden": false, "id": "autodate3332085495", "name": "updated", "onCreate": true, "onUpdate": true, "presentable": false, "system": false, "type": "autodate" } ], "id": "4szxr9x43tpj6np", "indexes": [ "CREATE INDEX ` + "`" + `idx_Jx8TXzDCmw` + "`" + ` ON ` + "`" + `certificate` + "`" + ` (` + "`" + `workflowRef` + "`" + `)", "CREATE INDEX ` + "`" + `idx_2cRXqNDyyp` + "`" + ` ON ` + "`" + `certificate` + "`" + ` (` + "`" + `workflowRunRef` + "`" + `)", "CREATE INDEX ` + "`" + `idx_kcKpgAZapk` + "`" + ` ON ` + "`" + `certificate` + "`" + ` (` + "`" + `workflowNodeId` + "`" + `)" ], "name": "certificate", "system": false, "type": "base" }, { "fields": [ { "autogeneratePattern": "[a-z0-9]{15}", "hidden": false, "id": "text3208210256", "max": 15, "min": 15, "name": "id", "pattern": "^[a-z0-9]+$", "presentable": false, "primaryKey": true, "required": true, "system": true, "type": "text" }, { "cascadeDelete": true, "collectionId": "tovyif5ax6j62ur", "hidden": false, "id": "m8xfsyyy", "maxSelect": 1, "minSelect": 0, "name": "workflowRef", "presentable": false, "required": false, "system": false, "type": "relation" }, { "hidden": false, "id": "qldmh0tw", "maxSelect": 1, "name": "status", "presentable": false, "required": false, "system": false, "type": "select", "values": [ "pending", "processing", "succeeded", "failed", "canceled" ] }, { "hidden": false, "id": "jlroa3fk", "maxSelect": 1, "name": "trigger", "presentable": false, "required": false, "system": false, "type": "select", "values": [ "manual", "scheduled" ] }, { "hidden": false, "id": "k9xvtf89", "max": "", "min": "", "name": "startedAt", "presentable": false, "required": false, "system": false, "type": "date" }, { "hidden": false, "id": "3ikum7mk", "max": "", "min": "", "name": "endedAt", "presentable": false, "required": false, "system": false, "type": "date" }, { "hidden": false, "id": "json772177811", "maxSize": 5000000, "name": "graph", "presentable": false, "required": false, "system": false, "type": "json" }, { "autogeneratePattern": "", "hidden": false, "id": "hvebkuxw", "max": 20000, "min": 0, "name": "error", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "hidden": false, "id": "autodate2990389176", "name": "created", "onCreate": true, "onUpdate": false, "presentable": false, "system": false, "type": "autodate" }, { "hidden": false, "id": "autodate3332085495", "name": "updated", "onCreate": true, "onUpdate": true, "presentable": false, "system": false, "type": "autodate" } ], "id": "qjp8lygssgwyqyz", "indexes": [ "CREATE INDEX ` + "`" + `idx_7ZpfjTFsD2` + "`" + ` ON ` + "`" + `workflow_run` + "`" + ` (` + "`" + `workflowRef` + "`" + `)" ], "name": "workflow_run", "system": false, "type": "base" }, { "fields": [ { "autogeneratePattern": "[a-z0-9]{15}", "hidden": false, "id": "text3208210256", "max": 15, "min": 15, "name": "id", "pattern": "^[a-z0-9]+$", "presentable": false, "primaryKey": true, "required": true, "system": true, "type": "text" }, { "cascadeDelete": true, "collectionId": "tovyif5ax6j62ur", "hidden": false, "id": "relation3371272342", "maxSelect": 1, "minSelect": 0, "name": "workflowRef", "presentable": false, "required": false, "system": false, "type": "relation" }, { "cascadeDelete": true, "collectionId": "qjp8lygssgwyqyz", "hidden": false, "id": "relation821863227", "maxSelect": 1, "minSelect": 0, "name": "runRef", "presentable": false, "required": false, "system": false, "type": "relation" }, { "autogeneratePattern": "", "hidden": false, "id": "text157423495", "max": 0, "min": 0, "name": "nodeId", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "autogeneratePattern": "", "hidden": false, "id": "text3227511481", "max": 0, "min": 0, "name": "nodeName", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "hidden": false, "id": "number2782324286", "max": null, "min": null, "name": "timestamp", "onlyInt": false, "presentable": false, "required": false, "system": false, "type": "number" }, { "hidden": false, "id": "number760395071", "max": null, "min": null, "name": "level", "onlyInt": false, "presentable": false, "required": false, "system": false, "type": "number" }, { "autogeneratePattern": "", "hidden": false, "id": "text3065852031", "max": 20000, "min": 0, "name": "message", "pattern": "", "presentable": false, "primaryKey": false, "required": false, "system": false, "type": "text" }, { "hidden": false, "id": "json2918445923", "maxSize": 5000000, "name": "data", "presentable": false, "required": false, "system": false, "type": "json" }, { "hidden": false, "id": "autodate2990389176", "name": "created", "onCreate": true, "onUpdate": false, "presentable": false, "system": false, "type": "autodate" } ], "id": "pbc_1682296116", "indexes": [ "CREATE INDEX ` + "`" + `idx_IOlpy6XuJ2` + "`" + ` ON ` + "`" + `workflow_logs` + "`" + ` (` + "`" + `workflowRef` + "`" + `)", "CREATE INDEX ` + "`" + `idx_qVlTb2yl7v` + "`" + ` ON ` + "`" + `workflow_logs` + "`" + ` (` + "`" + `runRef` + "`" + `)", "CREATE INDEX ` + "`" + `idx_UL4tdCXNlA` + "`" + ` ON ` + "`" + `workflow_logs` + "`" + ` (` + "`" + `nodeId` + "`" + `)" ], "name": "workflow_logs", "system": false, "type": "base" } ]` if err := app.ImportCollectionsByMarshaledJSON([]byte(jsonData), false); err != nil { return err } } // initialize superuser { collection, err := app.FindCollectionByNameOrId(core.CollectionNameSuperusers) if err != nil { return err } records, err := app.FindAllRecords(collection) if err != nil { return err } if len(records) == 0 { envUsername := strings.TrimSpace(os.Getenv("CERTIMATE_ADMIN_USERNAME")) if envUsername == "" { envUsername = "admin@certimate.fun" } envPassword := strings.TrimSpace(os.Getenv("CERTIMATE_ADMIN_PASSWORD")) if envPassword == "" { envPassword = "1234567890" } record := core.NewRecord(collection) record.Set("email", envUsername) record.Set("password", envPassword) return app.Save(record) } } // clean old migrations { migrations := []string{ "1739462400_collections_snapshot.go", "1739462401_superusers_initial.go", "1740050400_upgrade.go", "1742209200_upgrade.go", "1742392800_upgrade.go", "1742644800_upgrade.go", "1743264000_upgrade.go", "1744192800_upgrade.go", "1744459000_upgrade.go", "1745308800_upgrade.go", "1745726400_upgrade.go", "1747314000_upgrade.go", "1747389600_upgrade.go", "1748178000_upgrade.go", "1748228400_upgrade.go", "1748959200_upgrade.go", "1750687200_upgrade.go", "1751961600_upgrade.go", "1753272000_v0.4.0_migrate.go", "1755187200_cm0.4.0_migrate.go", "1756296000_cm0.4.0_migrate.go", "1757476800_cm0.4.0_initialize.go", "1757476800_m0.4.0_migrate.go", "1757476801_m0.4.0_initialize.go", "1760486400_m0.4.1.go", "1762142400_m0.4.3.go", "1762516800_m0.4.4.go", "1763373600_m0.4.5.go", "1763640000_m0.4.6.go", } for _, name := range migrations { app.DB().NewQuery("DELETE FROM _migrations WHERE file='" + name + "'").Execute() } } return nil }, func(app core.App) error { return nil }) } ================================================ FILE: migrations/1760486400_upgrade_v0.4.1.go ================================================ package migrations import ( "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" snaps "github.com/certimate-go/certimate/migrations/snaps/v0.4" ) func init() { m.Register(func(app core.App) error { if err := app.DB(). NewQuery("SELECT (1) FROM _migrations WHERE file={:file} LIMIT 1"). Bind(dbx.Params{"file": "1760486400_m0.4.1.go"}). One(&struct{}{}); err == nil { return nil } tracer := NewTracer("v0.4.1") tracer.Printf("go ...") // adapt to new workflow data structure { walker := &snaps.WorkflowGraphWalker{} walker.Define(func(node *snaps.WorkflowNode) (_changed bool, _err error) { _changed = false _err = nil if node.Type != "bizDeploy" { return } nodeCfg := node.Data.Config switch nodeCfg["provider"] { case "local": { if nodeCfg["providerAccessId"] != nil { delete(nodeCfg, "providerAccessId") _changed = true return } } } return }) // update collection `workflow` // - fix #982 { collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur") if err != nil { return err } records, err := app.FindAllRecords(collection) if err != nil { return err } for _, record := range records { changed := false if ret, err := walker.Migrate(record, "graphDraft"); err != nil { return err } else { changed = changed || ret } if ret, err := walker.Migrate(record, "graphContent"); err != nil { return err } else { changed = changed || ret } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } } } tracer.Printf("done") return nil }, func(app core.App) error { return nil }) } ================================================ FILE: migrations/1762142400_upgrade_v0.4.3.go ================================================ package migrations import ( "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" snaps "github.com/certimate-go/certimate/migrations/snaps/v0.4" ) func init() { m.Register(func(app core.App) error { if err := app.DB(). NewQuery("SELECT (1) FROM _migrations WHERE file={:file} LIMIT 1"). Bind(dbx.Params{"file": "1762142400_m0.4.3.go"}). One(&struct{}{}); err == nil { return nil } tracer := NewTracer("v0.4.3") tracer.Printf("go ...") // update collection `certificate` // - rename field `acmeRenewed` to `isRenewed` // - add field `isRevoked` // - add field `validityInterval` { collection, err := app.FindCollectionByNameOrId("4szxr9x43tpj6np") if err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(11, []byte(`{ "hidden": false, "id": "number2453290051", "max": null, "min": null, "name": "validityInterval", "onlyInt": false, "presentable": false, "required": false, "system": false, "type": "number" }`)); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(14, []byte(`{ "hidden": false, "id": "bool810050391", "name": "isRenewed", "presentable": false, "required": false, "system": false, "type": "bool" }`)); err != nil { return err } if err := collection.Fields.AddMarshaledJSONAt(15, []byte(`{ "hidden": false, "id": "bool3680845581", "name": "isRevoked", "presentable": false, "required": false, "system": false, "type": "bool" }`)); err != nil { return err } if err := app.Save(collection); err != nil { return err } if _, err := app.DB().NewQuery("UPDATE certificate SET validityInterval = (STRFTIME('%s', validityNotAfter) - STRFTIME('%s', validityNotBefore))").Execute(); err != nil { return err } tracer.Printf("collection '%s' updated", collection.Name) } // adapt to new workflow data structure { walker := &snaps.WorkflowGraphWalker{} walker.Define(func(node *snaps.WorkflowNode) (_changed bool, _err error) { _changed = false _err = nil if node.Type != "bizApply" { return } nodeCfg := node.Data.Config if nodeCfg["keySource"] == nil || nodeCfg["keySource"] == "" { nodeCfg["keySource"] = "auto" _changed = true return } return }) walker.Define(func(node *snaps.WorkflowNode) (_changed bool, _err error) { _changed = false _err = nil if node.Type != "bizUpload" { return } nodeCfg := node.Data.Config if nodeCfg["source"] == nil || nodeCfg["source"] == "" { nodeCfg["source"] = "form" _changed = true return } return }) // update collection `workflow` // - migrate field `graphDraft` / `graphContent` { collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur") if err != nil { return err } records, err := app.FindAllRecords(collection) if err != nil { return err } for _, record := range records { changed := false if ret, err := walker.Migrate(record, "graphDraft"); err != nil { return err } else { changed = changed || ret } if ret, err := walker.Migrate(record, "graphContent"); err != nil { return err } else { changed = changed || ret } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } } // update collection `workflow_run` // - migrate field `graph` { collection, err := app.FindCollectionByNameOrId("qjp8lygssgwyqyz") if err != nil { return err } records, err := app.FindAllRecords(collection) if err != nil { return err } for _, record := range records { changed := false if ret, err := walker.Migrate(record, "graph"); err != nil { return err } else { changed = changed || ret } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } } } tracer.Printf("done") return nil }, func(app core.App) error { return nil }) } ================================================ FILE: migrations/1762516800_upgrade_v0.4.4.go ================================================ package migrations import ( "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" ) func init() { m.Register(func(app core.App) error { if err := app.DB(). NewQuery("SELECT (1) FROM _migrations WHERE file={:file} LIMIT 1"). Bind(dbx.Params{"file": "1762516800_m0.4.4.go"}). One(&struct{}{}); err == nil { return nil } tracer := NewTracer("v0.4.4") tracer.Printf("go ...") // update collection `access` // - fix #1027 { if _, err := app.DB().NewQuery("UPDATE access SET provider = 'hostingde' WHERE provider = 'hostingDE'").Execute(); err != nil { return err } } // update collection `workflow` // - fix #1027 { if _, err := app.DB().NewQuery("UPDATE workflow SET graphDraft = REPLACE(graphDraft, '\"hostingDE\"', '\"hostingde\"')").Execute(); err != nil { return err } if _, err := app.DB().NewQuery("UPDATE workflow SET graphContent = REPLACE(graphContent, '\"hostingDE\"', '\"hostingde\"')").Execute(); err != nil { return err } } // update collection `settings` // - modify field `content` schema of `persistence` { collection, err := app.FindCollectionByNameOrId("dy6ccjb60spfy6p") if err != nil { return err } records, err := app.FindRecordsByFilter(collection, "name=\"persistence\"", "", 1, 0) if err != nil { return err } else if len(records) != 0 { record := records[0] changed := false content := make(map[string]any) if err := record.UnmarshalJSONField("content", &content); err != nil { return err } else { if _, ok := content["expiredCertificatesMaxDaysRetention"]; ok { content["certificatesRetentionMaxDays"] = content["expiredCertificatesMaxDaysRetention"] delete(content, "expiredCertificatesMaxDaysRetention") record.Set("content", content) changed = true } if _, ok := content["workflowRunsMaxDaysRetention"]; ok { content["workflowRunsRetentionMaxDays"] = content["workflowRunsMaxDaysRetention"] delete(content, "workflowRunsMaxDaysRetention") record.Set("content", content) changed = true } } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } } tracer.Printf("done") return nil }, func(app core.App) error { return nil }) } ================================================ FILE: migrations/1763373600_upgrade_v0.4.5.go ================================================ package migrations import ( "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" snaps "github.com/certimate-go/certimate/migrations/snaps/v0.4" ) func init() { m.Register(func(app core.App) error { if err := app.DB(). NewQuery("SELECT (1) FROM _migrations WHERE file={:file} LIMIT 1"). Bind(dbx.Params{"file": "1763373600_m0.4.5.go"}). One(&struct{}{}); err == nil { return nil } tracer := NewTracer("v0.4.5") tracer.Printf("go ...") // adapt to new workflow data structure { walker := &snaps.WorkflowGraphWalker{} walker.Define(func(node *snaps.WorkflowNode) (_changed bool, _err error) { _changed = false _err = nil if node.Type != "bizDeploy" { return } nodeCfg := node.Data.Config switch nodeCfg["provider"] { case "aliyun-waf": { if providerCfg, ok := nodeCfg["providerConfig"].(map[string]any); ok { providerCfg["serviceType"] = "cname" nodeCfg["providerConfig"] = providerCfg _changed = true return } } case "baishan-cdn": case "ksyun-cdn": case "rainyun-rcdn": { if providerCfg, ok := nodeCfg["providerConfig"].(map[string]any); ok { if providerCfg["certificateId"] != nil && providerCfg["certificateId"].(string) != "" { providerCfg["resourceType"] = "certificate" } else { providerCfg["resourceType"] = "domain" } nodeCfg["providerConfig"] = providerCfg _changed = true return } } case "tencentcloud-ssldeploy": { if providerCfg, ok := nodeCfg["providerConfig"].(map[string]any); ok { providerCfg["resourceProduct"] = providerCfg["resourceType"] delete(providerCfg, "resourceType") nodeCfg["providerConfig"] = providerCfg _changed = true return } } case "tencentcloud-sslupdate": { if providerCfg, ok := nodeCfg["providerConfig"].(map[string]any); ok { providerCfg["resourceProducts"] = providerCfg["resourceTypes"] delete(providerCfg, "resourceTypes") nodeCfg["providerConfig"] = providerCfg _changed = true return } } } return }) // update collection `workflow` // - migrate field `graphDraft` / `graphContent` { collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur") if err != nil { return err } records, err := app.FindAllRecords(collection) if err != nil { return err } for _, record := range records { changed := false if ret, err := walker.Migrate(record, "graphDraft"); err != nil { return err } else { changed = changed || ret } if ret, err := walker.Migrate(record, "graphContent"); err != nil { return err } else { changed = changed || ret } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } if _, err := app.DB().NewQuery("UPDATE workflow SET graphDraft = REPLACE(graphDraft, '\"matchPattern\"', '\"domainMatchPattern\"')").Execute(); err != nil { return err } if _, err := app.DB().NewQuery("UPDATE workflow SET graphContent = REPLACE(graphContent, '\"matchPattern\"', '\"domainMatchPattern\"')").Execute(); err != nil { return err } } // update collection `workflow_run` // - migrate field `graph` { collection, err := app.FindCollectionByNameOrId("qjp8lygssgwyqyz") if err != nil { return err } records, err := app.FindAllRecords(collection) if err != nil { return err } for _, record := range records { changed := false if ret, err := walker.Migrate(record, "graph"); err != nil { return err } else { changed = changed || ret } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } if _, err := app.DB().NewQuery("UPDATE workflow_run SET graph = REPLACE(graph, '\"matchPattern\"', '\"domainMatchPattern\"')").Execute(); err != nil { return err } } // update collection `workflow_output` // - migrate field `nodeConfig` { if _, err := app.DB().NewQuery("UPDATE workflow_output SET nodeConfig = REPLACE(nodeConfig, '\"matchPattern\"', '\"domainMatchPattern\"')").Execute(); err != nil { return err } if _, err := app.DB().NewQuery("UPDATE workflow_output SET nodeConfig = REPLACE(nodeConfig, '\"resourceType\"', '\"resourceProduct\"') WHERE nodeConfig LIKE '%\"provider\":\"tencentcloud-ssldeploy\"%' OR nodeConfig LIKE '%\"provider\":\"tencentcloud-sslupdate\"%'").Execute(); err != nil { return err } } } tracer.Printf("done") return nil }, func(app core.App) error { return nil }) } ================================================ FILE: migrations/1763640000_upgrade_v0.4.6.go ================================================ package migrations import ( "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" snaps "github.com/certimate-go/certimate/migrations/snaps/v0.4" ) func init() { m.Register(func(app core.App) error { if err := app.DB(). NewQuery("SELECT (1) FROM _migrations WHERE file={:file} LIMIT 1"). Bind(dbx.Params{"file": "1763640000_m0.4.6.go"}). One(&struct{}{}); err == nil { return nil } tracer := NewTracer("v0.4.6") tracer.Printf("go ...") // adapt to new workflow data structure { walker := &snaps.WorkflowGraphWalker{} walker.Define(func(node *snaps.WorkflowNode) (_changed bool, _err error) { _changed = false _err = nil if node.Type != "bizDeploy" { return } nodeCfg := node.Data.Config switch nodeCfg["provider"] { case "1panel-site": { if providerCfg, ok := nodeCfg["providerConfig"].(map[string]any); ok { if providerCfg["websiteId"] != nil && providerCfg["websiteId"].(string) != "" { providerCfg["websiteMatchPattern"] = "specified" nodeCfg["providerConfig"] = providerCfg _changed = true return } } } case "baotapanel-site": { if providerCfg, ok := nodeCfg["providerConfig"].(map[string]any); ok { if providerCfg["siteType"] == nil || providerCfg["siteType"].(string) == "other" { providerCfg["siteType"] = "any" nodeCfg["providerConfig"] = providerCfg _changed = true } if providerCfg["siteNames"] == nil || providerCfg["siteNames"].(string) == "" { providerCfg["siteNames"] = providerCfg["siteName"] delete(providerCfg, "siteName") nodeCfg["providerConfig"] = providerCfg _changed = true } if _changed { return } } } case "baotapanelgo-site": { if providerCfg, ok := nodeCfg["providerConfig"].(map[string]any); ok { if providerCfg["siteNames"] == nil || providerCfg["siteNames"].(string) == "" { providerCfg["siteType"] = "php" providerCfg["siteNames"] = providerCfg["siteName"] delete(providerCfg, "siteName") nodeCfg["providerConfig"] = providerCfg _changed = true return } } } case "baotawaf-site": { if providerCfg, ok := nodeCfg["providerConfig"].(map[string]any); ok { if providerCfg["siteNames"] == nil || providerCfg["siteNames"].(string) == "" { providerCfg["siteNames"] = providerCfg["siteName"] delete(providerCfg, "siteName") nodeCfg["providerConfig"] = providerCfg _changed = true return } } } case "ratpanel-site": { if providerCfg, ok := nodeCfg["providerConfig"].(map[string]any); ok { if providerCfg["siteNames"] == nil || providerCfg["siteNames"].(string) == "" { providerCfg["siteNames"] = providerCfg["siteName"] delete(providerCfg, "siteName") nodeCfg["providerConfig"] = providerCfg _changed = true return } } } case "safeline": { nodeCfg["provider"] = "safeline-site" _changed = true return } } return }) // update collection `workflow` // - migrate field `graphDraft` / `graphContent` { collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur") if err != nil { return err } records, err := app.FindAllRecords(collection) if err != nil { return err } for _, record := range records { changed := false if ret, err := walker.Migrate(record, "graphDraft"); err != nil { return err } else { changed = changed || ret } if ret, err := walker.Migrate(record, "graphContent"); err != nil { return err } else { changed = changed || ret } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } } // update collection `workflow_run` // - migrate field `graph` { collection, err := app.FindCollectionByNameOrId("qjp8lygssgwyqyz") if err != nil { return err } records, err := app.FindAllRecords(collection) if err != nil { return err } for _, record := range records { changed := false if ret, err := walker.Migrate(record, "graph"); err != nil { return err } else { changed = changed || ret } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } } // update collection `workflow_output` // - migrate field `nodeConfig` { if _, err := app.DB().NewQuery("UPDATE workflow_output SET nodeConfig = REPLACE(nodeConfig, '\"provider\":\"safeline\"', '\"provider\":\"safeline-site\"') WHERE nodeConfig LIKE '%\"provider\":\"safeline\"%'").Execute(); err != nil { return err } if _, err := app.DB().NewQuery("UPDATE workflow_output SET nodeConfig = REPLACE(nodeConfig, '\"siteName\":', '\"siteNames\":')").Execute(); err != nil { return err } } } tracer.Printf("done") return nil }, func(app core.App) error { return nil }) } ================================================ FILE: migrations/1766592000_upgrade_v0.4.11.go ================================================ package migrations import ( "net" "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" snaps "github.com/certimate-go/certimate/migrations/snaps/v0.4" ) func init() { m.Register(func(app core.App) error { tracer := NewTracer("v0.4.11") tracer.Printf("go ...") // adapt to new workflow data structure { walker := &snaps.WorkflowGraphWalker{} walker.Define(func(node *snaps.WorkflowNode) (_changed bool, _err error) { _changed = false _err = nil if node.Type != "bizApply" { return } nodeCfg := node.Data.Config if nodeCfg["identifier"] == nil || nodeCfg["identifier"] == "" { if nodeCfg["domains"] != nil && nodeCfg["domains"].(string) != "" { if ip := net.ParseIP(nodeCfg["domains"].(string)); ip != nil { nodeCfg["identifier"] = "ip" } else { nodeCfg["identifier"] = "domain" } _changed = true return } } return }) walker.Define(func(node *snaps.WorkflowNode) (_changed bool, _err error) { _changed = false _err = nil if node.Type != "bizUpload" { return } nodeCfg := node.Data.Config if nodeCfg["domains"] != nil { delete(nodeCfg, "domains") _changed = true return } return }) // update collection `workflow` // - migrate field `graphDraft` / `graphContent` { collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur") if err != nil { return err } records, err := app.FindAllRecords(collection) if err != nil { return err } for _, record := range records { changed := false if ret, err := walker.Migrate(record, "graphDraft"); err != nil { return err } else { changed = changed || ret } if ret, err := walker.Migrate(record, "graphContent"); err != nil { return err } else { changed = changed || ret } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } } // update collection `workflow_run` // - migrate field `graph` { collection, err := app.FindCollectionByNameOrId("qjp8lygssgwyqyz") if err != nil { return err } records, err := app.FindAllRecords(collection) if err != nil { return err } for _, record := range records { changed := false if ret, err := walker.Migrate(record, "graph"); err != nil { return err } else { changed = changed || ret } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } } } // clean old migrations { migrations := []string{ "1757476800_m0.4.0_migrate.go", "1757476801_m0.4.0_initialize.go", "1760486400_m0.4.1.go", "1762142400_m0.4.3.go", "1762516800_m0.4.4.go", "1763373600_m0.4.5.go", "1763553600_m0.4.6.go", "1763640000_m0.4.6.go", } for _, name := range migrations { app.DB().NewQuery("DELETE FROM _migrations WHERE file='" + name + "'").Execute() } } tracer.Printf("done") return nil }, func(app core.App) error { return nil }) } ================================================ FILE: migrations/1766800800_upgrade_v0.4.12.go ================================================ package migrations import ( "strings" "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcertx509 "github.com/certimate-go/certimate/pkg/utils/cert/x509" ) func init() { m.Register(func(app core.App) error { tracer := NewTracer("v0.4.12") tracer.Printf("go ...") // update collection `certificate` // - update field `subjectAltNames` { collection, err := app.FindCollectionByNameOrId("4szxr9x43tpj6np") if err != nil { return err } records, err := app.FindAllRecords(collection) if err != nil { return err } for _, record := range records { changed := false if certX509, err := xcert.ParseCertificateFromPEM(record.GetString("certificate")); err == nil { certSANs := xcertx509.GetSubjectAltNames(certX509) if strings.Join(certSANs, ";") != record.GetString("subjectAltNames") { record.Set("subjectAltNames", strings.Join(certSANs, ";")) changed = true } } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } } tracer.Printf("done") return nil }, func(app core.App) error { return nil }) } ================================================ FILE: migrations/1767024000_upgrade_v0.4.13.go ================================================ package migrations import ( "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" snaps "github.com/certimate-go/certimate/migrations/snaps/v0.4" ) func init() { m.Register(func(app core.App) error { tracer := NewTracer("v0.4.13") tracer.Printf("go ...") // adapt to new workflow data structure { walker := &snaps.WorkflowGraphWalker{} walker.Define(func(node *snaps.WorkflowNode) (_changed bool, _err error) { _changed = false _err = nil if node.Type != "bizDeploy" { return } nodeCfg := node.Data.Config switch nodeCfg["provider"] { case "1panel-site": { nodeCfg["provider"] = "1panel" _changed = true return } case "baotapanel-site": { nodeCfg["provider"] = "baotapanel" _changed = true return } case "baotapanelgo-site": { nodeCfg["provider"] = "baotapanelgo" _changed = true return } case "baotawaf-site": { nodeCfg["provider"] = "baotawaf" _changed = true return } case "cdnfly": { if providerCfg, ok := nodeCfg["providerConfig"].(map[string]any); ok { if providerCfg["resourceType"] == "site" { providerCfg["resourceType"] = "website" nodeCfg["providerConfig"] = providerCfg _changed = true return } } } case "cpanel-site": { if providerCfg, ok := nodeCfg["providerConfig"].(map[string]any); ok { providerCfg["resourceType"] = "website" nodeCfg["providerConfig"] = providerCfg } nodeCfg["provider"] = "cpanel" _changed = true return } case "netlify-site": { if providerCfg, ok := nodeCfg["providerConfig"].(map[string]any); ok { providerCfg["resourceType"] = "website" nodeCfg["providerConfig"] = providerCfg } nodeCfg["provider"] = "netlify" _changed = true return } case "ratpanel-site": { if providerCfg, ok := nodeCfg["providerConfig"].(map[string]any); ok { providerCfg["resourceType"] = "website" nodeCfg["providerConfig"] = providerCfg } nodeCfg["provider"] = "ratpanel" _changed = true return } case "safeline-site": { nodeCfg["provider"] = "safeline" _changed = true return } } return }) // update collection `workflow` // - migrate field `graphDraft` / `graphContent` { collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur") if err != nil { return err } records, err := app.FindAllRecords(collection) if err != nil { return err } for _, record := range records { changed := false if ret, err := walker.Migrate(record, "graphDraft"); err != nil { return err } else { changed = changed || ret } if ret, err := walker.Migrate(record, "graphContent"); err != nil { return err } else { changed = changed || ret } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } } // update collection `workflow_run` // - migrate field `graph` { collection, err := app.FindCollectionByNameOrId("qjp8lygssgwyqyz") if err != nil { return err } records, err := app.FindAllRecords(collection) if err != nil { return err } for _, record := range records { changed := false if ret, err := walker.Migrate(record, "graph"); err != nil { return err } else { changed = changed || ret } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } } } tracer.Printf("done") return nil }, func(app core.App) error { return nil }) } ================================================ FILE: migrations/1768363200_upgrade_v0.4.14.go ================================================ package migrations import ( "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" ) func init() { m.Register(func(app core.App) error { tracer := NewTracer("v0.4.14") tracer.Printf("go ...") // update collection `settings` // - modify field `content` schema of `sslProvider` { collection, err := app.FindCollectionByNameOrId("dy6ccjb60spfy6p") if err != nil { return err } records, err := app.FindRecordsByFilter(collection, "name=\"sslProvider\"", "", 1, 0) if err != nil { return err } else if len(records) != 0 { record := records[0] changed := false content := make(map[string]any) if err := record.UnmarshalJSONField("content", &content); err != nil { return err } else { if _, ok := content["config"]; ok { content["configs"] = content["config"] delete(content, "config") record.Set("content", content) changed = true } } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } } tracer.Printf("done") return nil }, func(app core.App) error { return nil }) } ================================================ FILE: migrations/1769313600_upgrade_v0.4.15.go ================================================ package migrations import ( "encoding/json" "strings" "github.com/go-viper/mapstructure/v2" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" snaps "github.com/certimate-go/certimate/migrations/snaps/v0.4" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcertx509 "github.com/certimate-go/certimate/pkg/utils/cert/x509" ) func init() { m.Register(func(app core.App) error { tracer := NewTracer("v0.4.15") tracer.Printf("go ...") // update collection `acme_accounts` // - rebuild indexes { collection, err := app.FindCollectionByNameOrId("012d7abbod1hwvr") if err != nil { return err } if err := json.Unmarshal([]byte(`{ "indexes": [ "CREATE INDEX `+"`"+`idx_dQiYzimY7m`+"`"+` ON `+"`"+`acme_accounts`+"`"+` (`+"`"+`ca`+"`"+`)", "CREATE INDEX `+"`"+`idx_TjyqY6LAGa`+"`"+` ON `+"`"+`acme_accounts`+"`"+` (\n `+"`"+`ca`+"`"+`,\n `+"`"+`acmeDirUrl`+"`"+`\n)", "CREATE UNIQUE INDEX `+"`"+`idx_G4brUDgxzc`+"`"+` ON `+"`"+`acme_accounts`+"`"+` (\n `+"`"+`ca`+"`"+`,\n `+"`"+`email`+"`"+`,\n `+"`"+`acmeAcctUrl`+"`"+`,\n `+"`"+`acmeDirUrl`+"`"+`\n)" ] }`), &collection); err != nil { return err } if err := app.Save(collection); err != nil { return err } tracer.Printf("collection '%s' updated", collection.Name) } // update collection `certificate` // - update field `subjectAltNames` // - remove field `acmeCertStableUrl` { collection, err := app.FindCollectionByNameOrId("4szxr9x43tpj6np") if err != nil { return err } collection.Fields.RemoveByName("acmeCertStableUrl") if err := app.Save(collection); err != nil { return err } tracer.Printf("collection '%s' updated", collection.Name) records, err := app.FindAllRecords(collection) if err != nil { return err } for _, record := range records { changed := false if certX509, err := xcert.ParseCertificateFromPEM(record.GetString("certificate")); err == nil { certSANs := xcertx509.GetSubjectAltNames(certX509) if strings.Join(certSANs, ";") != record.GetString("subjectAltNames") { record.Set("subjectAltNames", strings.Join(certSANs, ";")) changed = true } } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } } // update collection `workflow_output` // - revert data for #1137 { collection, err := app.FindCollectionByNameOrId("bqnxb95f2cooowp") if err != nil { return err } records, err := app.FindAllRecords(collection) if err != nil { return err } for _, record := range records { changed := false runRecord, _ := app.FindFirstRecordByFilter("workflow_run", "id={:runId}", dbx.Params{"runId": record.GetString("runRef")}) if runRecord != nil { runGraph := make(map[string]any) if err := runRecord.UnmarshalJSONField("graph", &runGraph); err != nil { return err } if _, ok := runGraph["nodes"]; ok { nodes := make([]*snaps.WorkflowNode, 0) if err := mapstructure.Decode(runGraph["nodes"], &nodes); err != nil { return err } nodeMaybeBrokenId := record.GetString("nodeId") var findNode func(blocks []*snaps.WorkflowNode) *snaps.WorkflowNode findNode = func(blocks []*snaps.WorkflowNode) *snaps.WorkflowNode { for _, node := range blocks { if node.Id == nodeMaybeBrokenId { return node } if len(node.Blocks) > 0 { if node := findNode(node.Blocks); node != nil { return node } } } return nil } if node := findNode(nodes); node != nil { continue } var findNodeEx func(blocks []*snaps.WorkflowNode) *snaps.WorkflowNode findNodeEx = func(blocks []*snaps.WorkflowNode) *snaps.WorkflowNode { for _, node := range blocks { const TRUNCATED_LENGTH = 3 // same as `ATTEMPTS` in '1757476800_upgrade_v0.4.0.go' if strings.HasSuffix(node.Id, nodeMaybeBrokenId) && (len(node.Id)-len(nodeMaybeBrokenId) == TRUNCATED_LENGTH) { return node } if len(node.Blocks) > 0 { if node := findNodeEx(node.Blocks); node != nil { return node } } } return nil } if node := findNodeEx(nodes); node != nil { record.Set("nodeId", node.Id) changed = true } } } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } } // adapt to new workflow data structure { walker := &snaps.WorkflowGraphWalker{} walker.Define(func(node *snaps.WorkflowNode) (_changed bool, _err error) { _changed = false _err = nil if node.Type != "bizDeploy" { return } nodeCfg := node.Data.Config if nodeCfg["provider"] == "rainyun-rcdn" { if providerCfg, ok := nodeCfg["providerConfig"].(map[string]any); ok { if providerCfg["resourceType"] == "certificate" { delete(providerCfg, "resourceType") delete(providerCfg, "instanceId") delete(providerCfg, "domainMatchPattern") delete(providerCfg, "domain") nodeCfg["provider"] = "rainyun-sslcenter" nodeCfg["providerConfig"] = providerCfg } else { delete(providerCfg, "resourceType") delete(providerCfg, "certificateId") nodeCfg["providerConfig"] = providerCfg } _changed = true return } } return }) // update collection `workflow` // - migrate field `graphDraft` / `graphContent` { collection, err := app.FindCollectionByNameOrId("tovyif5ax6j62ur") if err != nil { return err } records, err := app.FindAllRecords(collection) if err != nil { return err } for _, record := range records { changed := false if ret, err := walker.Migrate(record, "graphDraft"); err != nil { return err } else { changed = changed || ret } if ret, err := walker.Migrate(record, "graphContent"); err != nil { return err } else { changed = changed || ret } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } } // update collection `workflow_run` // - migrate field `graph` { collection, err := app.FindCollectionByNameOrId("qjp8lygssgwyqyz") if err != nil { return err } records, err := app.FindAllRecords(collection) if err != nil { return err } for _, record := range records { changed := false if ret, err := walker.Migrate(record, "graph"); err != nil { return err } else { changed = changed || ret } if changed { if err := app.Save(record); err != nil { return err } tracer.Printf("record #%s in collection '%s' updated", record.Id, collection.Name) } } } } tracer.Printf("done") return nil }, func(app core.App) error { return nil }) } ================================================ FILE: migrations/snaps/v0.3/workflow.go ================================================ package snaps // This is a definition backup of WorkflowNode for v0.3. type WorkflowNode struct { Id string `json:"id"` Type string `json:"type"` Name string `json:"name"` Config map[string]any `json:"config,omitempty"` Next *WorkflowNode `json:"next,omitempty"` Branches []*WorkflowNode `json:"branches,omitempty"` } ================================================ FILE: migrations/snaps/v0.4/workflow.go ================================================ package snaps import ( "fmt" "github.com/go-viper/mapstructure/v2" "github.com/pocketbase/pocketbase/core" ) type WorkflowGraphWalker struct { visitors []WorkflowNodeVisitor } type WorkflowNodeVisitor func(node *WorkflowNode) (_changed bool, _err error) func (w *WorkflowGraphWalker) Define(visitor WorkflowNodeVisitor) { if w.visitors == nil { w.visitors = make([]WorkflowNodeVisitor, 0) } w.visitors = append(w.visitors, visitor) } func (w *WorkflowGraphWalker) Visit(nodes []*WorkflowNode) (_changed bool, _err error) { changed := false if w.visitors == nil { return changed, nil } for _, node := range nodes { for _, visitor := range w.visitors { nodeChanged, err := visitor(node) if err != nil { return changed, err } if nodeChanged { changed = true } if len(node.Blocks) > 0 { blocksChanged, err := w.Visit(node.Blocks) if err != nil { return changed, err } if blocksChanged { changed = true } } } } return changed, nil } func (w *WorkflowGraphWalker) Migrate(record *core.Record, field string) (_changed bool, _err error) { f := record.Collection().Fields.GetByName(field) if f == nil { return false, fmt.Errorf("field '%s' not found", field) } if record.GetRaw(field) != nil { graph := make(map[string]any) if err := record.UnmarshalJSONField(field, &graph); err != nil { return false, err } if _, ok := graph["nodes"]; ok { nodes := make([]*WorkflowNode, 0) if err := mapstructure.Decode(graph["nodes"], &nodes); err != nil { return false, err } nodesChanged, err := w.Visit(nodes) if err != nil { return false, err } else if nodesChanged { graph["nodes"] = nodes record.Set(field, graph) return true, nil } } } return false, nil } // This is a definition copy of WorkflowNode. // see: /internal/domain/workflow.go type WorkflowNode struct { Id string `json:"id"` Type string `json:"type"` Data WorkflowNodeData `json:"data"` Blocks WorkflowNodeBlocks `json:"blocks,omitempty,omitzero"` } // This is a definition copy of []*WorkflowNode. // see: /internal/domain/workflow.go type WorkflowNodeBlocks []*WorkflowNode // This is a definition copy of WorkflowNodeData. // see: /internal/domain/workflow.go type WorkflowNodeData struct { Name string `json:"name"` Disabled bool `json:"disabled,omitempty,omitzero"` Config WorkflowNodeConfig `json:"config,omitempty,omitzero"` } // This is a definition copy of WorkflowNodeConfig. // see: /internal/domain/workflow.go type WorkflowNodeConfig map[string]any func (g WorkflowNodeBlocks) GetNodeById(nodeId string) (*WorkflowNode, bool) { return g.getNodeInBlocksById(g, nodeId) } func (g WorkflowNodeBlocks) getNodeInBlocksById(blocks WorkflowNodeBlocks, nodeId string) (*WorkflowNode, bool) { for _, node := range blocks { if node.Id == nodeId { return node, true } if len(node.Blocks) > 0 { if found, ok := g.getNodeInBlocksById(node.Blocks, nodeId); ok { return found, true } } } return nil, false } ================================================ FILE: migrations/tracer.go ================================================ package migrations import ( "fmt" "log/slog" ) type Tracer struct { logger *slog.Logger flag string } func NewTracer(flag string) *Tracer { return &Tracer{ logger: slog.Default(), flag: flag, } } func (l *Tracer) Printf(format string, args ...any) { l.logger.Info("[CERTIMATE] migration " + l.flag + ": " + fmt.Sprintf(format, args...)) } ================================================ FILE: pkg/core/certifier/challenger.go ================================================ package certifier import ( "github.com/go-acme/lego/v4/challenge" ) type ACMEChallenger = challenge.Provider ================================================ FILE: pkg/core/certifier/challengers/dns01/35cn/35cn.go ================================================ package west35cn import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/com35" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { Username string `json:"username"` ApiPassword string `json:"apiPassword"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := com35.NewDefaultConfig() providerConfig.Username = config.Username providerConfig.Password = config.ApiPassword if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := com35.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/51dnscom/51dnscom.go ================================================ package dnscom import ( "errors" "time" "github.com/certimate-go/certimate/pkg/core/certifier" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/51dnscom/internal" ) type ChallengerConfig struct { ApiKey string `json:"apiKey"` ApiSecret string `json:"apiSecret"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := internal.NewDefaultConfig() providerConfig.APIKey = config.ApiKey providerConfig.APISecret = config.ApiSecret if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := internal.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/51dnscom/internal/lego.go ================================================ package internal import ( "errors" "fmt" "sync" "time" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/samber/lo" dnscomsdk "github.com/certimate-go/certimate/pkg/sdk3rd/51dnscom" ) const ( envNamespace = "51DNSCOM_" EnvAPIKey = envNamespace + "API_KEY" EnvAPISecret = envNamespace + "API_SECRET" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) var _ challenge.ProviderTimeout = (*DNSProvider)(nil) type Config struct { APIKey string APISecret string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPTimeout time.Duration } type DNSProvider struct { config *Config client *dnscomsdk.Client recordCache map[string]dnsRecordCacheEntry // Key: ChallengeToken recordCacheMu sync.Mutex } func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), } } func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIKey, EnvAPISecret) if err != nil { return nil, fmt.Errorf("51dnscom: %w", err) } config := NewDefaultConfig() config.APIKey = values[EnvAPIKey] config.APISecret = values[EnvAPISecret] return NewDNSProviderConfig(config) } func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("51dnscom: the configuration of the DNS provider is nil") } client, err := dnscomsdk.NewClient(config.APIKey, config.APISecret) if err != nil { return nil, fmt.Errorf("51dnscom: %w", err) } else { client.SetTimeout(config.HTTPTimeout) } return &DNSProvider{ config: config, client: client, recordCache: make(map[string]dnsRecordCacheEntry), recordCacheMu: sync.Mutex{}, }, nil } func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { return fmt.Errorf("51dnscom: could not find zone for domain %q: %w", domain, err) } subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) if err != nil { return fmt.Errorf("51dnscom: %w", err) } zone, err := d.findZone(dns01.UnFqdn(authZone)) if err != nil { return fmt.Errorf("51dnscom: error when list zones: %w", err) } // REF: https://www.51dns.com/document/api/4/12.html request := &dnscomsdk.RecordCreateRequest{ DomainID: lo.ToPtr(zone.DomainID.String()), Type: lo.ToPtr("TXT"), Host: lo.ToPtr(subDomain), Value: lo.ToPtr(info.Value), TTL: lo.ToPtr(int32(d.config.TTL)), } response, err := d.client.RecordCreate(request) if err != nil { return fmt.Errorf("51dnscom: error when create record: %w", err) } d.recordCacheMu.Lock() d.recordCache[token] = dnsRecordCacheEntry{DomainID: zone.DomainID.String(), RecordID: response.Data.RecordID.String()} d.recordCacheMu.Unlock() return nil } func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) d.recordCacheMu.Lock() record, ok := d.recordCache[token] d.recordCacheMu.Unlock() if !ok { return fmt.Errorf("51dnscom: unknown record ID for '%s'", info.EffectiveFQDN) } // REF: https://www.51dns.com/document/api/4/27.html request := &dnscomsdk.RecordRemoveRequest{ DomainID: lo.ToPtr(record.DomainID), RecordID: lo.ToPtr(record.RecordID), } if _, err := d.client.RecordRemove(request); err != nil { return fmt.Errorf("51dnscom: error when delete record: %w", err) } return nil } func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } type dnsRecordCacheEntry struct { DomainID string RecordID string } func (d *DNSProvider) findZone(zoneName string) (*dnscomsdk.DomainRecord, error) { page := 1 pageSize := 10 for { // REF: https://www.51dns.com/document/api/74/88.html request := &dnscomsdk.DomainListRequest{ Page: lo.ToPtr(int32(page)), PageSize: lo.ToPtr(int32(pageSize)), } response, err := d.client.DomainList(request) if err != nil { return nil, err } if response.Data == nil { break } for _, domainItem := range response.Data.Data { if domainItem.Domain == zoneName { return domainItem, nil } } if len(response.Data.Data) < pageSize || response.Data.PageCount <= int32(page) { break } page++ } return nil, fmt.Errorf("could not find zone '%s'", zoneName) } ================================================ FILE: pkg/core/certifier/challengers/dns01/acmedns/acmedns.go ================================================ package acmedns import ( "errors" "fmt" "os" "github.com/go-acme/lego/v4/providers/dns/acmedns" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { ServerUrl string `json:"serverUrl"` Credentials string `json:"credentials"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } tempfile, err := os.CreateTemp("", "certimate.acmedns_*.tmp") if err != nil { return nil, fmt.Errorf("failed to create temp credentials file: %w", err) } else { if _, err := tempfile.Write([]byte(config.Credentials)); err != nil { return nil, fmt.Errorf("failed to write temp credentials file: %w", err) } tempfile.Close() } providerConfig := acmedns.NewDefaultConfig() providerConfig.APIBase = config.ServerUrl providerConfig.StoragePath = tempfile.Name() provider, err := acmedns.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/acmehttpreq/acmehttpreq.go ================================================ package acmehttpreq import ( "errors" "net/url" "time" "github.com/go-acme/lego/v4/providers/dns/httpreq" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { Endpoint string `json:"endpoint"` Mode string `json:"mode"` Username string `json:"username"` Password string `json:"password"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } endpoint, _ := url.Parse(config.Endpoint) providerConfig := httpreq.NewDefaultConfig() providerConfig.Endpoint = endpoint providerConfig.Mode = config.Mode providerConfig.Username = config.Username providerConfig.Password = config.Password if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } provider, err := httpreq.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/akamai-edgedns/akamai_edgedns.go ================================================ package akamaiedgedns import ( "time" "github.com/akamai/AkamaiOPEN-edgegrid-golang/v11/pkg/edgegrid" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/providers/dns/edgedns" ) type ChallengerConfig struct { Host string ClientToken string ClientSecret string AccessToken string DnsPropagationTimeout int DnsTTL int } func NewChallenger(config *ChallengerConfig) (challenge.Provider, error) { edgegridConfig := &edgegrid.Config{ Host: config.Host, ClientToken: config.ClientToken, ClientSecret: config.ClientSecret, AccessToken: config.AccessToken, MaxBody: 131072, HeaderToSign: []string{ "X-Akamai-ACS-Action", "X-Akamai-ACS-Auth-Data", "X-Akamai-ACS-Auth-Sign", }, } providerConfig := edgedns.NewDefaultConfig() providerConfig.Config = edgegridConfig if config.DnsPropagationTimeout > 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL > 0 { providerConfig.TTL = config.DnsTTL } provider, err := edgedns.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/aliyun/aliyun.go ================================================ package aliyun import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/alidns" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { AccessKeyId string `json:"accessKeyId"` AccessKeySecret string `json:"accessKeySecret"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := alidns.NewDefaultConfig() providerConfig.APIKey = config.AccessKeyId providerConfig.SecretKey = config.AccessKeySecret if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := alidns.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/aliyun-esa/aliyun_esa.go ================================================ package aliyunesa import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/aliesa" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { AccessKeyId string `json:"accessKeyId"` AccessKeySecret string `json:"accessKeySecret"` Region string `json:"region"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := aliesa.NewDefaultConfig() providerConfig.APIKey = config.AccessKeyId providerConfig.SecretKey = config.AccessKeySecret providerConfig.RegionID = config.Region if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := aliesa.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/arvancloud/arvancloud.go ================================================ package arvancloud import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/arvancloud" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { ApiKey string `json:"apiKey"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := arvancloud.NewDefaultConfig() providerConfig.APIKey = config.ApiKey if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := arvancloud.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/aws-route53/aws-route53.go ================================================ package awsroute53 import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/route53" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { AccessKeyId string `json:"accessKeyId"` SecretAccessKey string `json:"secretAccessKey"` Region string `json:"region"` HostedZoneId string `json:"hostedZoneId,omitempty"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := route53.NewDefaultConfig() providerConfig.AccessKeyID = config.AccessKeyId providerConfig.SecretAccessKey = config.SecretAccessKey providerConfig.Region = config.Region if config.HostedZoneId != "" { providerConfig.HostedZoneID = config.HostedZoneId } if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := route53.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/azure-dns/azure-dns.go ================================================ package azuredns import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/azuredns" "github.com/certimate-go/certimate/pkg/core/certifier" azenv "github.com/certimate-go/certimate/pkg/sdk3rd/azure/env" ) type ChallengerConfig struct { TenantId string `json:"tenantId"` ClientId string `json:"clientId"` ClientSecret string `json:"clientSecret"` SubscriptionId string `json:"subscriptionId,omitempty"` ResourceGroupName string `json:"resourceGroupName,omitempty"` CloudName string `json:"cloudName,omitempty"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := azuredns.NewDefaultConfig() providerConfig.AuthMethod = "env" providerConfig.TenantID = config.TenantId providerConfig.ClientID = config.ClientId providerConfig.ClientSecret = config.ClientSecret providerConfig.SubscriptionID = config.SubscriptionId providerConfig.ResourceGroup = config.ResourceGroupName if config.CloudName != "" { env, err := azenv.GetCloudEnvConfiguration(config.CloudName) if err != nil { return nil, err } providerConfig.Environment = env } if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := azuredns.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/baiducloud/baiducloud.go ================================================ package baiducloud import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/baiducloud" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { AccessKeyId string `json:"accessKeyId"` SecretAccessKey string `json:"secretAccessKey"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := baiducloud.NewDefaultConfig() providerConfig.AccessKeyID = config.AccessKeyId providerConfig.SecretAccessKey = config.SecretAccessKey if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := baiducloud.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/bookmyname/bookmyname.go ================================================ package bookmyname import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/bookmyname" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { Username string `json:"username"` Password string `json:"password"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := bookmyname.NewDefaultConfig() providerConfig.Username = config.Username providerConfig.Password = config.Password if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := bookmyname.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/bunny/bunny.go ================================================ package bunny import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/bunny" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { ApiKey string `json:"apiKey"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := bunny.NewDefaultConfig() providerConfig.APIKey = config.ApiKey if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := bunny.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/cloudflare/cloudflare.go ================================================ package cloudflare import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/cloudflare" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { DnsApiToken string `json:"dnsApiToken"` ZoneApiToken string `json:"zoneApiToken,omitempty"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := cloudflare.NewDefaultConfig() providerConfig.AuthToken = config.DnsApiToken providerConfig.ZoneToken = config.ZoneApiToken if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := cloudflare.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/cloudns/cloudns.go ================================================ package cloudns import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/cloudns" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { AuthId string `json:"authId"` AuthPassword string `json:"authPassword"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := cloudns.NewDefaultConfig() providerConfig.AuthID = config.AuthId providerConfig.AuthPassword = config.AuthPassword if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := cloudns.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/cmcccloud/cmcccloud.go ================================================ package cmcccloud import ( "errors" "time" "github.com/certimate-go/certimate/pkg/core/certifier" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/cmcccloud/internal" ) type ChallengerConfig struct { AccessKeyId string `json:"accessKeyId"` AccessKeySecret string `json:"accessKeySecret"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := internal.NewDefaultConfig() providerConfig.AccessKey = config.AccessKeyId providerConfig.SecretKey = config.AccessKeySecret if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } provider, err := internal.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/cmcccloud/internal/lego.go ================================================ package internal import ( "errors" "fmt" "sync" "time" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/samber/lo" "gitlab.ecloud.com/ecloud/ecloudsdkclouddns" "gitlab.ecloud.com/ecloud/ecloudsdkclouddns/model" "gitlab.ecloud.com/ecloud/ecloudsdkcore/config" ) const ( envNamespace = "CMCCCLOUD_" EnvAccessKey = envNamespace + "ACCESS_KEY" EnvSecretKey = envNamespace + "SECRET_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvReadTimeout = envNamespace + "READ_TIMEOUT" EnvConnectTimeout = envNamespace + "CONNECT_TIMEOUT" ) var _ challenge.ProviderTimeout = (*DNSProvider)(nil) type Config struct { AccessKey string SecretKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int ReadTimeout int ConnectTimeout int } type DNSProvider struct { config *Config client *ecloudsdkclouddns.Client recordIDs map[string]string // Key: ChallengeToken; Value: RecordID recordIDsMu sync.Mutex } func NewDefaultConfig() *Config { return &Config{ ReadTimeout: env.GetOrDefaultInt(EnvReadTimeout, 30), ConnectTimeout: env.GetOrDefaultInt(EnvConnectTimeout, 30), TTL: env.GetOrDefaultInt(EnvTTL, 600), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), } } func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAccessKey, EnvSecretKey) if err != nil { return nil, fmt.Errorf("cmccecloud: %w", err) } cfg := NewDefaultConfig() cfg.AccessKey = values[EnvAccessKey] cfg.SecretKey = values[EnvSecretKey] return NewDNSProviderConfig(cfg) } func NewDNSProviderConfig(cfg *Config) (*DNSProvider, error) { if cfg == nil { return nil, errors.New("cmccecloud: the configuration of the DNS provider is nil") } client := ecloudsdkclouddns.NewClient(&config.Config{ AccessKey: cfg.AccessKey, SecretKey: cfg.SecretKey, // 资源池常量见: https://ecloud.10086.cn/op-help-center/doc/article/54462 // 默认全局 PoolId: "CIDC-CORE-00", ReadTimeOut: cfg.ReadTimeout, ConnectTimeout: cfg.ConnectTimeout, }) return &DNSProvider{ config: cfg, client: client, recordIDs: make(map[string]string), recordIDsMu: sync.Mutex{}, }, nil } func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) zoneName, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { return fmt.Errorf("cmccecloud: could not find zone for domain %q: %w", domain, err) } subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zoneName) if err != nil { return fmt.Errorf("cmccecloud: %w", err) } request := &model.CreateRecordOpenapiRequest{ CreateRecordOpenapiBody: &model.CreateRecordOpenapiBody{ LineId: "0", // 默认线路 Rr: subDomain, DomainName: dns01.UnFqdn(zoneName), Description: "certimate acme", Type: model.CreateRecordOpenapiBodyTypeEnumTxt, Value: info.Value, Ttl: lo.ToPtr(int32(d.config.TTL)), }, } response, err := d.client.CreateRecordOpenapi(request) if err != nil { return fmt.Errorf("cmccecloud: error when create record: %w", err) } else if response.State != model.CreateRecordOpenapiResponseStateEnumOk { return fmt.Errorf("cmccecloud: failed to create record: unexpected response state: '%s', errcode: '%s', errmsg: '%s'", response.State, response.ErrorCode, response.ErrorMessage) } d.recordIDsMu.Lock() d.recordIDs[token] = response.Body.RecordId d.recordIDsMu.Unlock() return nil } func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("cmccecloud: unknown record ID for '%s'", info.EffectiveFQDN) } request := &model.DeleteRecordOpenapiRequest{ DeleteRecordOpenapiBody: &model.DeleteRecordOpenapiBody{ RecordIdList: []string{recordID}, }, } response, err := d.client.DeleteRecordOpenapi(request) if err != nil { return fmt.Errorf("cmccecloud: error when delete record: %w", err) } else if response.State != model.DeleteRecordOpenapiResponseStateEnumOk { return fmt.Errorf("cmccecloud: failed to delete record, unexpected response state: '%s', errcode: '%s', errmsg: '%s'", response.State, response.ErrorCode, response.ErrorMessage) } return nil } func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } ================================================ FILE: pkg/core/certifier/challengers/dns01/constellix/constellix.go ================================================ package cloudns import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/constellix" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { ApiKey string `json:"apiKey"` SecretKey string `json:"secretKey"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := constellix.NewDefaultConfig() providerConfig.APIKey = config.ApiKey providerConfig.SecretKey = config.SecretKey if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := constellix.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/cpanel/cpanel.go ================================================ package cpanel import ( "crypto/tls" "errors" "time" "github.com/go-acme/lego/v4/providers/dns/cpanel" "github.com/certimate-go/certimate/pkg/core/certifier" xhttp "github.com/certimate-go/certimate/pkg/utils/http" ) type ChallengerConfig struct { ServerUrl string `json:"serverUrl"` Username string `json:"username"` ApiToken string `json:"apiToken"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := cpanel.NewDefaultConfig() providerConfig.Mode = "cpanel" providerConfig.BaseURL = config.ServerUrl providerConfig.Username = config.Username providerConfig.Token = config.ApiToken if config.AllowInsecureConnections { transport := xhttp.NewDefaultTransport() transport.DisableKeepAlives = true transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} providerConfig.HTTPClient.Transport = transport } if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := cpanel.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/ctcccloud/ctcccloud.go ================================================ package ctcccloud import ( "errors" "time" "github.com/certimate-go/certimate/pkg/core/certifier" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/ctcccloud/internal" ) type ChallengerConfig struct { AccessKeyId string `json:"accessKeyId"` SecretAccessKey string `json:"secretAccessKey"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := internal.NewDefaultConfig() providerConfig.AccessKeyId = config.AccessKeyId providerConfig.SecretAccessKey = config.SecretAccessKey if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } provider, err := internal.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/ctcccloud/internal/lego.go ================================================ package internal import ( "errors" "fmt" "sync" "time" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/samber/lo" ctyundns "github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/dns" ) const ( envNamespace = "CTYUNSMARTDNS_" EnvAccessKeyID = envNamespace + "ACCESS_KEY_ID" EnvSecretAccessKey = envNamespace + "SECRET_ACCESS_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) var _ challenge.ProviderTimeout = (*DNSProvider)(nil) type Config struct { AccessKeyId string SecretAccessKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPTimeout time.Duration } type DNSProvider struct { client *ctyundns.Client config *Config recordIDs map[string]int32 // Key: ChallengeToken; Value: RecordID recordIDsMu sync.Mutex } func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 600), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute), HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), } } func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAccessKeyID, EnvSecretAccessKey) if err != nil { return nil, fmt.Errorf("ctyun: %w", err) } config := NewDefaultConfig() config.AccessKeyId = values[EnvAccessKeyID] config.SecretAccessKey = values[EnvSecretAccessKey] return NewDNSProviderConfig(config) } func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("ctyun: the configuration of the DNS provider is nil") } client, err := ctyundns.NewClient(config.AccessKeyId, config.SecretAccessKey) if err != nil { return nil, fmt.Errorf("ctyun: %w", err) } else { client.SetTimeout(config.HTTPTimeout) } return &DNSProvider{ client: client, config: config, recordIDs: make(map[string]int32), recordIDsMu: sync.Mutex{}, }, nil } func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { return fmt.Errorf("ctyun: could not find zone for domain %q: %w", domain, err) } subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) if err != nil { return fmt.Errorf("ctyun: %w", err) } // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=122&api=11259&data=181&isNormal=1&vid=259 request := &ctyundns.AddRecordRequest{ Domain: lo.ToPtr(dns01.UnFqdn(authZone)), Host: lo.ToPtr(subDomain), Type: lo.ToPtr("TXT"), LineCode: lo.ToPtr("Default"), Value: lo.ToPtr(info.Value), State: lo.ToPtr(int32(1)), TTL: lo.ToPtr(int32(d.config.TTL)), } response, err := d.client.AddRecord(request) if err != nil { return fmt.Errorf("ctyun: error when create record: %w", err) } d.recordIDsMu.Lock() d.recordIDs[token] = response.ReturnObj.RecordId d.recordIDsMu.Unlock() return nil } func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("tencentcloud-eo: unknown record ID for '%s'", info.EffectiveFQDN) } // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=122&api=11262&data=181&isNormal=1&vid=259 request := &ctyundns.DeleteRecordRequest{ RecordId: lo.ToPtr(recordID), } if _, err := d.client.DeleteRecord(request); err != nil { return fmt.Errorf("ctyun: error when delete record: %w", err) } return nil } func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } ================================================ FILE: pkg/core/certifier/challengers/dns01/desec/desec.go ================================================ package desec import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/desec" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { Token string `json:"token"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := desec.NewDefaultConfig() providerConfig.Token = config.Token if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := desec.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/digitalocean/digitalocean.go ================================================ package namedotcom import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/digitalocean" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { AccessToken string `json:"accessToken"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := digitalocean.NewDefaultConfig() providerConfig.AuthToken = config.AccessToken if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := digitalocean.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/dnsexit/dnsexit.go ================================================ package dnsexit import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/dnsexit" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { ApiKey string `json:"apiKey"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := dnsexit.NewDefaultConfig() providerConfig.APIKey = config.ApiKey if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := dnsexit.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/dnsla/dnsla.go ================================================ package dnsla import ( "errors" "time" "github.com/certimate-go/certimate/pkg/core/certifier" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/dnsla/internal" ) type ChallengerConfig struct { ApiId string `json:"apiId"` ApiSecret string `json:"apiSecret"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := internal.NewDefaultConfig() providerConfig.APIId = config.ApiId providerConfig.APISecret = config.ApiSecret if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := internal.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/dnsla/internal/lego.go ================================================ package internal import ( "errors" "fmt" "strings" "sync" "time" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/samber/lo" dnslasdk "github.com/certimate-go/certimate/pkg/sdk3rd/dnsla" ) const ( envNamespace = "DNSLA_" EnvAPIId = envNamespace + "API_ID" EnvAPISecret = envNamespace + "API_SECRET" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) var _ challenge.ProviderTimeout = (*DNSProvider)(nil) type Config struct { APIId string APISecret string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPTimeout time.Duration } type DNSProvider struct { config *Config client *dnslasdk.Client recordIDs map[string]string // Key: ChallengeToken; Value: RecordID recordIDsMu sync.Mutex } func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 300), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), } } func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAPIId, EnvAPISecret) if err != nil { return nil, fmt.Errorf("dnsla: %w", err) } config := NewDefaultConfig() config.APIId = values[EnvAPIId] config.APISecret = values[EnvAPISecret] return NewDNSProviderConfig(config) } func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("dnsla: the configuration of the DNS provider is nil") } client, err := dnslasdk.NewClient(config.APIId, config.APISecret) if err != nil { return nil, fmt.Errorf("dnsla: %w", err) } else { client.SetTimeout(config.HTTPTimeout) } return &DNSProvider{ config: config, client: client, recordIDs: make(map[string]string), recordIDsMu: sync.Mutex{}, }, nil } func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { return fmt.Errorf("dnsla: could not find zone for domain %q: %w", domain, err) } subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) if err != nil { return fmt.Errorf("dnsla: %w", err) } zone, err := d.findZone(dns01.UnFqdn(authZone)) if err != nil { return fmt.Errorf("dnsla: error when list zones: %w", err) } // REF: https://www.dnsla.cn/docs/ApiDoc request := &dnslasdk.CreateRecordRequest{ DomainId: lo.ToPtr(zone.Id), Type: lo.ToPtr(int32(16)), Host: lo.ToPtr(subDomain), Data: lo.ToPtr(info.Value), Ttl: lo.ToPtr(int32(d.config.TTL)), } response, err := d.client.CreateRecord(request) if err != nil { return fmt.Errorf("dnsla: error when create record: %w", err) } d.recordIDsMu.Lock() d.recordIDs[token] = response.Data.Id d.recordIDsMu.Unlock() return nil } func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("dnsla: unknown record ID for '%s'", info.EffectiveFQDN) } // REF: https://www.dnsla.cn/docs/ApiDoc if _, err := d.client.DeleteRecord(recordID); err != nil { return fmt.Errorf("dnsla: error when delete record: %w", err) } return nil } func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } func (d *DNSProvider) findZone(zoneName string) (*dnslasdk.DomainRecord, error) { pageIndex := 1 pageSize := 100 for { // REF: https://www.dnsla.cn/docs/ApiDoc request := &dnslasdk.ListDomainsRequest{ PageIndex: lo.ToPtr(int32(pageIndex)), PageSize: lo.ToPtr(int32(pageSize)), } response, err := d.client.ListDomains(request) if err != nil { return nil, err } if response.Data == nil { break } for _, domainItem := range response.Data.Results { if strings.TrimRight(domainItem.Domain, ".") == zoneName || strings.TrimRight(domainItem.DisplayDomain, ".") == zoneName { return domainItem, nil } } if len(response.Data.Results) < pageSize { break } pageIndex++ } return nil, fmt.Errorf("could not find zone '%s'", zoneName) } ================================================ FILE: pkg/core/certifier/challengers/dns01/dnsmadeeasy/dnsmadeeasy.go ================================================ package dnsmadeeasy import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/dnsmadeeasy" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { ApiKey string `json:"apiKey"` ApiSecret string `json:"apiSecret"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := dnsmadeeasy.NewDefaultConfig() providerConfig.APIKey = config.ApiKey providerConfig.APISecret = config.ApiSecret if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := dnsmadeeasy.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/duckdns/duckdns.go ================================================ package namedotcom import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/duckdns" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { Token string `json:"token"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := duckdns.NewDefaultConfig() providerConfig.Token = config.Token if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } provider, err := duckdns.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/dynu/dynu.go ================================================ package dynu import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/dynu" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { ApiKey string `json:"apiKey"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := dynu.NewDefaultConfig() providerConfig.APIKey = config.ApiKey if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := dynu.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/dynv6/dynv6.go ================================================ package dynv6 import ( "errors" "time" "github.com/certimate-go/certimate/pkg/core/certifier" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/dynv6/internal" ) type ChallengerConfig struct { HttpToken string `json:"httpToken"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := internal.NewDefaultConfig() providerConfig.HTTPToken = config.HttpToken if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := internal.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/dynv6/internal/lego.go ================================================ package internal import ( "errors" "fmt" "sync" "time" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/samber/lo" dynv6sdk "github.com/certimate-go/certimate/pkg/sdk3rd/dynv6" ) const ( envNamespace = "DYNV6_" EnvHTTPToken = envNamespace + "HTTP_TOKEN" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) var _ challenge.ProviderTimeout = (*DNSProvider)(nil) type Config struct { HTTPToken string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPTimeout time.Duration } type DNSProvider struct { config *Config client *dynv6sdk.Client zoneIDs map[string]int64 // Key: ZoneName; Value: ZoneID zoneIDsMu sync.Mutex recordIDs map[string]int64 // Key: ChallengeToken; Value: RecordID recordIDsMu sync.Mutex } func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), } } func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvHTTPToken) if err != nil { return nil, fmt.Errorf("dynv6: %w", err) } config := NewDefaultConfig() config.HTTPToken = values[EnvHTTPToken] return NewDNSProviderConfig(config) } func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("dynv6: the configuration of the DNS provider is nil") } client, err := dynv6sdk.NewClient(config.HTTPToken) if err != nil { return nil, fmt.Errorf("dnsexit: %w", err) } else { client.SetTimeout(config.HTTPTimeout) } return &DNSProvider{ config: config, client: client, zoneIDs: make(map[string]int64), zoneIDsMu: sync.Mutex{}, recordIDs: make(map[string]int64), recordIDsMu: sync.Mutex{}, }, nil } func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { return fmt.Errorf("dynv6: could not find zone for domain %q: %w", domain, err) } subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) if err != nil { return fmt.Errorf("dynv6: %w", err) } zone, err := d.findZone(dns01.UnFqdn(authZone)) if err != nil { return fmt.Errorf("dynv6: error when list zones: %w", err) } // REF: https://dynv6.github.io/api-spec/#tag/records/operation/addRecord response, err := d.client.AddRecord(zone.ID, &dynv6sdk.AddRecordRequest{ Type: lo.ToPtr("TXT"), Name: lo.ToPtr(subDomain), Data: lo.ToPtr(info.Value), }) if err != nil { return fmt.Errorf("dynv6: error when create record: %w", err) } d.zoneIDsMu.Lock() d.zoneIDs[zone.Name] = zone.ID d.zoneIDsMu.Unlock() d.recordIDsMu.Lock() d.recordIDs[token] = response.ID d.recordIDsMu.Unlock() return nil } func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { return fmt.Errorf("dynv6: could not find zone for domain %q: %w", domain, err) } d.zoneIDsMu.Lock() zoneId, ok := d.zoneIDs[dns01.UnFqdn(authZone)] d.zoneIDsMu.Unlock() if !ok { return fmt.Errorf("dynv6: unknown zone ID for '%s'", dns01.UnFqdn(authZone)) } d.recordIDsMu.Lock() recordId, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("dynv6: unknown record ID for '%s'", info.EffectiveFQDN) } if _, err := d.client.DeleteRecord(zoneId, recordId); err != nil { return fmt.Errorf("dynv6: error when delete record: %w", err) } return nil } func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } func (d *DNSProvider) findZone(zoneName string) (*dynv6sdk.ZoneRecord, error) { // REF: https://dynv6.github.io/api-spec/#tag/zones/operation/findZones zones, err := d.client.ListZones() if err != nil { return nil, err } for _, zone := range *zones { if zone.Name == zoneName { return zone, nil } } return nil, fmt.Errorf("could not find zone: '%s'", zoneName) } ================================================ FILE: pkg/core/certifier/challengers/dns01/gandinet/gandinet.go ================================================ package gandinet import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/gandiv5" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { PersonalAccessToken string `json:"personalAccessToken"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := gandiv5.NewDefaultConfig() providerConfig.PersonalAccessToken = config.PersonalAccessToken if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := gandiv5.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/gcore/gcore.go ================================================ package gcore import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/gcore" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { ApiToken string `json:"apiToken"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := gcore.NewDefaultConfig() providerConfig.APIToken = config.ApiToken if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := gcore.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/gname/gname.go ================================================ package gname import ( "errors" "time" "github.com/certimate-go/certimate/pkg/core/certifier" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/gname/internal" ) type ChallengerConfig struct { AppId string `json:"appId"` AppKey string `json:"appKey"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := internal.NewDefaultConfig() providerConfig.AppID = config.AppId providerConfig.AppKey = config.AppKey if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := internal.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/gname/internal/lego.go ================================================ package internal import ( "errors" "fmt" "sync" "time" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/samber/lo" gnamesdk "github.com/certimate-go/certimate/pkg/sdk3rd/gname" ) const ( envNamespace = "GNAME_" EnvAppID = envNamespace + "APP_ID" EnvAppKey = envNamespace + "APP_KEY" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) var _ challenge.ProviderTimeout = (*DNSProvider)(nil) type Config struct { AppID string AppKey string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPTimeout time.Duration } type DNSProvider struct { config *Config client *gnamesdk.Client recordIDs map[string]int64 // Key: ChallengeToken; Value: RecordID recordIDsMu sync.Mutex } func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 300), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), } } func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAppID, EnvAppKey) if err != nil { return nil, fmt.Errorf("gname: %w", err) } config := NewDefaultConfig() config.AppID = values[EnvAppID] config.AppKey = values[EnvAppKey] return NewDNSProviderConfig(config) } func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("gname: the configuration of the DNS provider is nil") } client, err := gnamesdk.NewClient(config.AppID, config.AppKey) if err != nil { return nil, fmt.Errorf("gname: %w", err) } else { client.SetTimeout(config.HTTPTimeout) } return &DNSProvider{ config: config, client: client, recordIDs: make(map[string]int64), recordIDsMu: sync.Mutex{}, }, nil } func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { return fmt.Errorf("gname: could not find zone for domain %q: %w", domain, err) } subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) if err != nil { return fmt.Errorf("gname: %w", err) } // REF: https://www.gname.vip/domain/api/dns/add request := &gnamesdk.AddDomainResolutionRequest{ ZoneName: lo.ToPtr(dns01.UnFqdn(authZone)), RecordType: lo.ToPtr("TXT"), RecordName: lo.ToPtr(subDomain), RecordValue: lo.ToPtr(info.Value), TTL: lo.ToPtr(int32(d.config.TTL)), } response, err := d.client.AddDomainResolution(request) if err != nil { return fmt.Errorf("gname: error when create record: %w", err) } d.recordIDsMu.Lock() d.recordIDs[token], _ = response.Data.Int64() d.recordIDsMu.Unlock() return nil } func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { return fmt.Errorf("gname: could not find zone for domain %q: %w", domain, err) } d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("gname: unknown record ID for '%s'", info.EffectiveFQDN) } // REF: https://www.gname.vip/domain/api/dns/del request := &gnamesdk.DeleteDomainResolutionRequest{ ZoneName: lo.ToPtr(dns01.UnFqdn(authZone)), RecordID: lo.ToPtr(recordID), } _, err = d.client.DeleteDomainResolution(request) if err != nil { return fmt.Errorf("gname: error when delete record: %w", err) } return nil } func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } ================================================ FILE: pkg/core/certifier/challengers/dns01/godaddy/godaddy.go ================================================ package godaddy import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/godaddy" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { ApiKey string `json:"apiKey"` ApiSecret string `json:"apiSecret"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := godaddy.NewDefaultConfig() providerConfig.APIKey = config.ApiKey providerConfig.APISecret = config.ApiSecret if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := godaddy.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/hetzner/hetzner.go ================================================ package hetzner import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/hetzner" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { ApiToken string `json:"apiToken"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := hetzner.NewDefaultConfig() providerConfig.APIToken = config.ApiToken if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := hetzner.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/hostingde/hostingde.go ================================================ package hostingde import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/hostingde" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { ApiKey string `json:"apiKey"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := hostingde.NewDefaultConfig() providerConfig.APIKey = config.ApiKey if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := hostingde.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/hostinger/hostinger.go ================================================ package hostinger import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/hostinger" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { ApiToken string `json:"apiToken"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := hostinger.NewDefaultConfig() providerConfig.APIToken = config.ApiToken if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := hostinger.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/huaweicloud/huaweicloud.go ================================================ package huaweicloud import ( "errors" "time" "github.com/certimate-go/certimate/pkg/core/certifier" hwc "github.com/go-acme/lego/v4/providers/dns/huaweicloud" ) type ChallengerConfig struct { AccessKeyId string `json:"accessKeyId"` SecretAccessKey string `json:"secretAccessKey"` Region string `json:"region"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } region := config.Region if region == "" { // 华为云的 SDK 要求必须传一个区域,实际上 DNS 服务用不到,但不传会报错 region = "cn-north-1" } providerConfig := hwc.NewDefaultConfig() providerConfig.AccessKeyID = config.AccessKeyId providerConfig.SecretAccessKey = config.SecretAccessKey providerConfig.Region = region if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = int32(config.DnsTTL) } provider, err := hwc.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/infomaniak/infomaniak.go ================================================ package infomaniak import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/infomaniak" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { AccessToken string `json:"accessToken"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := infomaniak.NewDefaultConfig() providerConfig.AccessToken = config.AccessToken if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := infomaniak.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/ionos/ionos.go ================================================ package ionos import ( "errors" "fmt" "time" "github.com/go-acme/lego/v4/providers/dns/ionos" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { ApiKeyPublicPrefix string `json:"apiKeyPublicPrefix"` ApiKeySecret string `json:"apiKeySecret"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := ionos.NewDefaultConfig() providerConfig.APIKey = fmt.Sprintf("%s.%s", config.ApiKeyPublicPrefix, config.ApiKeySecret) if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := ionos.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/jdcloud/jdcloud.go ================================================ package jdcloud import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/jdcloud" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { AccessKeyId string `json:"accessKeyId"` AccessKeySecret string `json:"accessKeySecret"` RegionId string `json:"regionId"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } regionId := config.RegionId if regionId == "" { // 京东云的 SDK 要求必须传一个区域,实际上 DNS 服务用不到,但不传会报错 regionId = "cn-north-1" } providerConfig := jdcloud.NewDefaultConfig() providerConfig.AccessKeyID = config.AccessKeyId providerConfig.AccessKeySecret = config.AccessKeySecret providerConfig.RegionID = regionId if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := jdcloud.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/linode/linode.go ================================================ package linode import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/linode" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { AccessToken string `json:"accessToken"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := linode.NewDefaultConfig() providerConfig.Token = config.AccessToken if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := linode.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/namecheap/namecheap.go ================================================ package namedotcom import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/namecheap" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { Username string `json:"username"` ApiKey string `json:"apiKey"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := namecheap.NewDefaultConfig() providerConfig.APIUser = config.Username providerConfig.APIKey = config.ApiKey if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := namecheap.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/namedotcom/namedotcom.go ================================================ package namedotcom import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/namedotcom" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { Username string `json:"username"` ApiToken string `json:"apiToken"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := namedotcom.NewDefaultConfig() providerConfig.Username = config.Username providerConfig.APIToken = config.ApiToken if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := namedotcom.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/namesilo/namesilo.go ================================================ package namesilo import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/namesilo" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { ApiKey string `json:"apiKey"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := namesilo.NewDefaultConfig() providerConfig.APIKey = config.ApiKey if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := namesilo.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/netcup/netcup.go ================================================ package netcup import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/netcup" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { CustomerNumber string `json:"customerNumber"` ApiKey string `json:"apiKey"` ApiPassword string `json:"apiPassword"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := netcup.NewDefaultConfig() providerConfig.Customer = config.CustomerNumber providerConfig.Key = config.ApiKey providerConfig.Password = config.ApiPassword if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := netcup.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/netlify/netlify.go ================================================ package netcup import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/netlify" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { ApiToken string `json:"apiToken"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := netlify.NewDefaultConfig() providerConfig.Token = config.ApiToken if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := netlify.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/ns1/ns1.go ================================================ package ns1 import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/ns1" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { ApiKey string `json:"apiKey"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := ns1.NewDefaultConfig() providerConfig.APIKey = config.ApiKey if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := ns1.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/ovhcloud/consts.go ================================================ package ovhcloud const ( AUTH_METHOD_APPLICATION = "application" AUTH_METHOD_OAUTH2 = "oauth2" ) ================================================ FILE: pkg/core/certifier/challengers/dns01/ovhcloud/ovhcloud.go ================================================ package ovhcloud import ( "errors" "fmt" "time" "github.com/go-acme/lego/v4/providers/dns/ovh" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { Endpoint string `json:"endpoint"` AuthMethod string `json:"authMethod"` ApplicationKey string `json:"applicationKey,omitempty"` ApplicationSecret string `json:"applicationSecret,omitempty"` ConsumerKey string `json:"consumerKey,omitempty"` ClientId string `json:"clientId,omitempty"` ClientSecret string `json:"clientSecret,omitempty"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := ovh.NewDefaultConfig() providerConfig.APIEndpoint = config.Endpoint switch config.AuthMethod { case AUTH_METHOD_APPLICATION: providerConfig.ApplicationKey = config.ApplicationKey providerConfig.ApplicationSecret = config.ApplicationSecret providerConfig.ConsumerKey = config.ConsumerKey case AUTH_METHOD_OAUTH2: providerConfig.OAuth2Config = &ovh.OAuth2Config{ ClientID: config.ClientId, ClientSecret: config.ClientSecret, } default: return nil, fmt.Errorf("unsupported auth method '%s'", config.AuthMethod) } if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := ovh.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/porkbun/porkbun.go ================================================ package porkbun import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/porkbun" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { ApiKey string `json:"apiKey"` SecretApiKey string `json:"secretApiKey"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := porkbun.NewDefaultConfig() providerConfig.APIKey = config.ApiKey providerConfig.SecretAPIKey = config.SecretApiKey if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := porkbun.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/powerdns/powerdns.go ================================================ package powerdns import ( "crypto/tls" "errors" "net/url" "time" "github.com/go-acme/lego/v4/providers/dns/pdns" "github.com/certimate-go/certimate/pkg/core/certifier" xhttp "github.com/certimate-go/certimate/pkg/utils/http" ) type ChallengerConfig struct { ServerUrl string `json:"serverUrl"` ApiKey string `json:"apiKey"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } serverUrl, _ := url.Parse(config.ServerUrl) providerConfig := pdns.NewDefaultConfig() providerConfig.Host = serverUrl providerConfig.APIKey = config.ApiKey if config.AllowInsecureConnections { transport := xhttp.NewDefaultTransport() transport.DisableKeepAlives = true transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} providerConfig.HTTPClient.Transport = transport } if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := pdns.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/qingcloud/internal/lego.go ================================================ package internal import ( "errors" "fmt" "sync" "time" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/samber/lo" qingcloudsdk "github.com/certimate-go/certimate/pkg/sdk3rd/qingcloud/dns" ) const ( envNamespace = "QINGCLOUD_" EnvAccessKey = envNamespace + "ACCESS_KEY" EnvAccessSecret = envNamespace + "ACCESS_SECRET" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) var _ challenge.ProviderTimeout = (*DNSProvider)(nil) type Config struct { AccessKey string AccessSecret string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPTimeout time.Duration } type DNSProvider struct { config *Config client *qingcloudsdk.Client recordIDs map[string]*int64 // Key: ChallengeToken; Value: RecordID recordIDsMu sync.Mutex } func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), } } func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAccessKey, EnvAccessSecret) if err != nil { return nil, fmt.Errorf("qingcloud: %w", err) } config := NewDefaultConfig() config.AccessKey = values[EnvAccessKey] config.AccessSecret = values[EnvAccessSecret] return NewDNSProviderConfig(config) } func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("qingcloud: the configuration of the DNS provider is nil") } client, err := qingcloudsdk.NewClient(config.AccessKey, config.AccessSecret) if err != nil { return nil, fmt.Errorf("qingcloud: %w", err) } else { client.SetTimeout(config.HTTPTimeout) } return &DNSProvider{ config: config, client: client, recordIDs: make(map[string]*int64), recordIDsMu: sync.Mutex{}, }, nil } func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { return fmt.Errorf("qingcloud: could not find zone for domain %q: %w", domain, err) } // REF: https://docsv4.qingcloud.com/user_guide/development_docs/api/api_list/dns/record/#_createrecord request := &qingcloudsdk.CreateRecordRequest{ ZoneName: lo.ToPtr(authZone), DomainName: lo.ToPtr(info.EffectiveFQDN), ViewId: lo.ToPtr(int32(0)), Type: lo.ToPtr("TXT"), Ttl: lo.ToPtr(int32(d.config.TTL)), Records: []*qingcloudsdk.CreateRecordRequestRecord{ { Values: []*qingcloudsdk.CreateRecordRequestRecordValue{ { Value: lo.ToPtr(info.Value), Status: lo.ToPtr(int32(1)), }, }, Weight: lo.ToPtr(int32(0)), }, }, Mode: lo.ToPtr(int32(1)), AutoMerge: lo.ToPtr(int32(1)), } response, err := d.client.CreateRecord(request) if err != nil { return fmt.Errorf("qingcloud: error when create record: %w", err) } d.recordIDsMu.Lock() d.recordIDs[token] = response.DomainRecordId d.recordIDsMu.Unlock() return nil } func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("qingcloud: unknown record ID for '%s'", info.EffectiveFQDN) } // REF: https://docsv4.qingcloud.com/user_guide/development_docs/api/api_list/dns/record/#_deleterecord if _, err := d.client.DeleteRecord([]*int64{recordID}); err != nil { return fmt.Errorf("qingcloud: error when delete record: %w", err) } return nil } func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } ================================================ FILE: pkg/core/certifier/challengers/dns01/qingcloud/qingcloud.go ================================================ package qingcloud import ( "errors" "time" "github.com/certimate-go/certimate/pkg/core/certifier" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/qingcloud/internal" ) type ChallengerConfig struct { AccessKeyId string `json:"accessKeyId"` SecretAccessKey string `json:"apiPassword"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := internal.NewDefaultConfig() providerConfig.AccessKey = config.AccessKeyId providerConfig.AccessSecret = config.SecretAccessKey if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := internal.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/rainyun/rainyun.go ================================================ package rainyun import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/rainyun" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { ApiKey string `json:"apiKey"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := rainyun.NewDefaultConfig() providerConfig.APIKey = config.ApiKey if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := rainyun.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/rfc2136/rfc2136.go ================================================ package rfc2136 import ( "errors" "net" "strconv" "time" "github.com/go-acme/lego/v4/providers/dns/rfc2136" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { Host string `json:"host"` Port int32 `json:"port"` TsigAlgorithm string `json:"tsigAlgorithm,omitempty"` TsigKey string `json:"tsigKey,omitempty"` TsigSecret string `json:"tsigSecret,omitempty"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } if config.Port == 0 { config.Port = 53 } if config.TsigAlgorithm == "" { config.TsigAlgorithm = "hmac-sha1." } providerConfig := rfc2136.NewDefaultConfig() providerConfig.Nameserver = net.JoinHostPort(config.Host, strconv.Itoa(int(config.Port))) providerConfig.TSIGAlgorithm = config.TsigAlgorithm providerConfig.TSIGKey = config.TsigKey providerConfig.TSIGSecret = config.TsigSecret if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := rfc2136.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/spaceship/spaceship.go ================================================ package spaceship import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/spaceship" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { ApiKey string `json:"apiKey"` ApiSecret string `json:"apiSecret"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := spaceship.NewDefaultConfig() providerConfig.APIKey = config.ApiKey providerConfig.APISecret = config.ApiSecret if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := spaceship.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/technitiumdns/technitiumdns.go ================================================ package technitiumdns import ( "crypto/tls" "errors" "time" "github.com/go-acme/lego/v4/providers/dns/technitium" "github.com/certimate-go/certimate/pkg/core/certifier" xhttp "github.com/certimate-go/certimate/pkg/utils/http" ) type ChallengerConfig struct { ServerUrl string `json:"serverUrl"` ApiToken string `json:"apiToken"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := technitium.NewDefaultConfig() providerConfig.BaseURL = config.ServerUrl providerConfig.APIToken = config.ApiToken if config.AllowInsecureConnections { transport := xhttp.NewDefaultTransport() transport.DisableKeepAlives = true transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} providerConfig.HTTPClient.Transport = transport } if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := technitium.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/tencentcloud/tencentcloud.go ================================================ package tencentcloud import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/tencentcloud" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { SecretId string `json:"secretId"` SecretKey string `json:"secretKey"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := tencentcloud.NewDefaultConfig() providerConfig.SecretID = config.SecretId providerConfig.SecretKey = config.SecretKey if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := tencentcloud.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/tencentcloud-eo/tencentcloud_eo.go ================================================ package tencentcloudeo import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/edgeone" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { SecretId string `json:"secretId"` SecretKey string `json:"secretKey"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := edgeone.NewDefaultConfig() providerConfig.SecretID = config.SecretId providerConfig.SecretKey = config.SecretKey if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := edgeone.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/todaynic/todaynic.go ================================================ package todaynic import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/todaynic" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { UserId string `json:"userId"` ApiKey string `json:"apiKey"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := todaynic.NewDefaultConfig() providerConfig.AuthUserID = config.UserId providerConfig.APIKey = config.ApiKey if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := todaynic.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/ucloud/internal/lego.go ================================================ package internal import ( "errors" "fmt" "time" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/ucloud/ucloud-sdk-go/ucloud" "github.com/ucloud/ucloud-sdk-go/ucloud/auth" "github.com/certimate-go/certimate/pkg/sdk3rd/ucloud/udnr" ) const ( envNamespace = "UCLOUDUDNR_" EnvPublicKey = envNamespace + "PUBLIC_KEY" EnvPrivateKey = envNamespace + "PRIVATE_KEY" EnvProjectId = envNamespace + "PROJECT_ID" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) var _ challenge.ProviderTimeout = (*DNSProvider)(nil) type Config struct { PrivateKey string PublicKey string ProjectId string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPTimeout time.Duration } type DNSProvider struct { config *Config client *udnr.UDNRClient } func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 600), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), } } func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvPrivateKey, EnvPublicKey, EnvProjectId) if err != nil { return nil, fmt.Errorf("ucloud: %w", err) } config := NewDefaultConfig() config.PrivateKey = values[EnvPrivateKey] config.PublicKey = values[EnvPublicKey] config.ProjectId = values[EnvProjectId] return NewDNSProviderConfig(config) } func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("ucloud: the configuration of the DNS provider is nil") } cfg := ucloud.NewConfig() cfg.Timeout = config.HTTPTimeout cfg.ProjectId = config.ProjectId credential := auth.NewCredential() credential.PrivateKey = config.PrivateKey credential.PublicKey = config.PublicKey client := udnr.NewClient(&cfg, &credential) return &DNSProvider{ config: config, client: client, }, nil } func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { return fmt.Errorf("ucloud: could not find zone for domain %q: %w", domain, err) } // REF: https://docs.ucloud.cn/api/udnr-api/udnr_domain_dns_add request := d.client.NewAddDomainDNSRequest() request.Dn = ucloud.String(dns01.UnFqdn(authZone)) request.DnsType = ucloud.String("TXT") request.RecordName = ucloud.String(dns01.UnFqdn(info.EffectiveFQDN)) request.Content = ucloud.String(info.Value) request.TTL = ucloud.String(fmt.Sprintf("%d", d.config.TTL)) if _, err := d.client.AddDomainDNS(request); err != nil { return fmt.Errorf("ucloud: error when create record: %w", err) } return nil } func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { return fmt.Errorf("ucloud: could not find zone for domain %q: %w", domain, err) } // REF: https://docs.ucloud.cn/api/udnr-api/udnr_domain_dns_query request := d.client.NewQueryDomainDNSRequest() request.Dn = ucloud.String(dns01.UnFqdn(authZone)) response, err := d.client.QueryDomainDNS(request) if err != nil { return fmt.Errorf("ucloud: error when list records: %w", err) } // REF: https://docs.ucloud.cn/api/udnr-api/udnr_delete_dns_record for _, record := range response.Data { if record.DnsType == "TXT" && record.RecordName == dns01.UnFqdn(info.EffectiveFQDN) && record.Content == info.Value { delreq := d.client.NewDeleteDomainDNSRequest() delreq.Dn = ucloud.String(dns01.UnFqdn(authZone)) delreq.DnsType = ucloud.String(record.DnsType) delreq.RecordName = ucloud.String(record.RecordName) delreq.Content = ucloud.String(record.Content) _, err := d.client.DeleteDomainDNS(delreq) if err != nil { return fmt.Errorf("ucloud: error when delete record: %w", err) } break } } return nil } func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } ================================================ FILE: pkg/core/certifier/challengers/dns01/ucloud/ucloud.go ================================================ package ucloud import ( "errors" "time" "github.com/certimate-go/certimate/pkg/core/certifier" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/ucloud/internal" ) type ChallengerConfig struct { PrivateKey string `json:"privateKey"` PublicKey string `json:"publicKey"` ProjectId string `json:"projectId,omitempty"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("config is nil") } providerConfig := internal.NewDefaultConfig() providerConfig.PrivateKey = config.PrivateKey providerConfig.PublicKey = config.PublicKey providerConfig.ProjectId = config.ProjectId if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } provider, err := internal.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/vercel/vercel.go ================================================ package vercel import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/vercel" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { ApiAccessToken string `json:"apiAccessToken"` TeamId string `json:"teamId,omitempty"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := vercel.NewDefaultConfig() providerConfig.AuthToken = config.ApiAccessToken providerConfig.TeamID = config.TeamId if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := vercel.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/volcengine/volcengine.go ================================================ package volcengine import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/volcengine" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { AccessKeyId string `json:"accessKeyId"` SecretAccessKey string `json:"secretAccessKey"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := volcengine.NewDefaultConfig() providerConfig.AccessKey = config.AccessKeyId providerConfig.SecretKey = config.SecretAccessKey if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := volcengine.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/vultr/vultr.go ================================================ package vultr import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/vultr" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { ApiKey string `json:"apiKey"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := vultr.NewDefaultConfig() providerConfig.APIKey = config.ApiKey if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := vultr.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/westcn/westcn.go ================================================ package westcn import ( "errors" "time" "github.com/go-acme/lego/v4/providers/dns/westcn" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { Username string `json:"username"` ApiPassword string `json:"apiPassword"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := westcn.NewDefaultConfig() providerConfig.Username = config.Username providerConfig.Password = config.ApiPassword if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := westcn.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/dns01/xinnet/internal/lego.go ================================================ package internal import ( "errors" "fmt" "sync" "time" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/samber/lo" xinnetsdk "github.com/certimate-go/certimate/pkg/sdk3rd/xinnet" ) const ( envNamespace = "XINNET_" EnvAgentId = envNamespace + "AGENT_ID" EnvAppSecret = envNamespace + "APP_SECRET" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" ) var _ challenge.ProviderTimeout = (*DNSProvider)(nil) type Config struct { AgentID string AppSecret string PropagationTimeout time.Duration PollingInterval time.Duration TTL int HTTPTimeout time.Duration } type DNSProvider struct { config *Config client *xinnetsdk.Client recordIDs map[string]*int64 // Key: ChallengeToken; Value: RecordID recordIDsMu sync.Mutex } func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt(EnvTTL, 600), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), } } func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvAgentId, EnvAppSecret) if err != nil { return nil, fmt.Errorf("xinnet: %w", err) } config := NewDefaultConfig() config.AgentID = values[EnvAgentId] config.AppSecret = values[EnvAppSecret] return NewDNSProviderConfig(config) } func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("xinnet: the configuration of the DNS provider is nil") } client, err := xinnetsdk.NewClient(config.AgentID, config.AppSecret) if err != nil { return nil, fmt.Errorf("xinnet: %w", err) } else { client.SetTimeout(config.HTTPTimeout) } return &DNSProvider{ config: config, client: client, recordIDs: make(map[string]*int64), recordIDsMu: sync.Mutex{}, }, nil } func (d *DNSProvider) Present(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { return fmt.Errorf("xinnet: could not find zone for domain %q: %w", domain, err) } // REF: https://apidoc.xin.cn/doc-7283900 request := &xinnetsdk.DnsCreateRequest{ DomainName: lo.ToPtr(dns01.UnFqdn(authZone)), RecordName: lo.ToPtr(dns01.UnFqdn(info.EffectiveFQDN)), Type: lo.ToPtr("TXT"), Value: lo.ToPtr(info.Value), Line: lo.ToPtr("默认"), Ttl: lo.ToPtr(int32(d.config.TTL)), } response, err := d.client.DnsCreate(request) if err != nil { return fmt.Errorf("xinnet: error when create record: %w", err) } d.recordIDsMu.Lock() d.recordIDs[token] = response.Data d.recordIDsMu.Unlock() return nil } func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { info := dns01.GetChallengeInfo(domain, keyAuth) authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) if err != nil { return fmt.Errorf("xinnet: could not find zone for domain %q: %w", domain, err) } d.recordIDsMu.Lock() recordID, ok := d.recordIDs[token] d.recordIDsMu.Unlock() if !ok { return fmt.Errorf("xinnet: unknown record ID for '%s'", info.EffectiveFQDN) } // REF: https://apidoc.xin.cn/doc-7283901 request := &xinnetsdk.DnsDeleteRequest{ DomainName: lo.ToPtr(dns01.UnFqdn(authZone)), RecordId: recordID, } if _, err := d.client.DnsDelete(request); err != nil { return fmt.Errorf("xinnet: error when delete record: %w", err) } return nil } func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } ================================================ FILE: pkg/core/certifier/challengers/dns01/xinnet/xinnet.go ================================================ package xinnet import ( "errors" "time" "github.com/certimate-go/certimate/pkg/core/certifier" "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/xinnet/internal" ) type ChallengerConfig struct { AgentId string `json:"agentId"` ApiPassword string `json:"apiPassword"` DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` DnsTTL int `json:"dnsTTL,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } providerConfig := internal.NewDefaultConfig() providerConfig.AgentID = config.AgentId providerConfig.AppSecret = config.ApiPassword if config.DnsPropagationTimeout != 0 { providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second } if config.DnsTTL != 0 { providerConfig.TTL = config.DnsTTL } provider, err := internal.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/http01/local/local.go ================================================ package local import ( "errors" "github.com/go-acme/lego/v4/providers/http/webroot" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { // 网站根目录路径。 WebRootPath string `json:"webRootPath"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } provider, err := webroot.NewHTTPProvider(config.WebRootPath) if err != nil { return nil, err } return provider, nil } ================================================ FILE: pkg/core/certifier/challengers/http01/s3/s3.go ================================================ package s3 import ( "context" "errors" "fmt" "strings" "github.com/go-acme/lego/v4/challenge/http01" "github.com/certimate-go/certimate/internal/tools/s3" "github.com/certimate-go/certimate/pkg/core/certifier" ) type ChallengerConfig struct { // S3 Endpoint。 Endpoint string `json:"endpoint"` // S3 AccessKey。 AccessKey string `json:"accessKey"` // S3 SecretKey。 SecretKey string `json:"secretKey"` // S3 签名版本。 // 可取值 "v2"、"v4"。 // 零值时默认值 "v4"。 SignatureVersion string `json:"signatureVersion,omitempty"` // 是否使用路径风格。 UsePathStyle bool `json:"usePathStyle,omitempty"` // 存储区域。 Region string `json:"region"` // 存储桶名。 Bucket string `json:"bucket"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } client, err := createS3Client(*config) if err != nil { return nil, fmt.Errorf("s3: failed to create S3 client: %w", err) } provider := &provider{client: client, bucket: config.Bucket} return provider, nil } type provider struct { client *s3.Client bucket string } func (p *provider) Present(domain, token, keyAuth string) error { objectKey := strings.Trim(http01.ChallengePath(token), "/") if err := p.client.PutObjectString(context.Background(), p.bucket, objectKey, keyAuth); err != nil { return fmt.Errorf("s3: failed to upload token to s3: %w", err) } return nil } func (p *provider) CleanUp(domain, token, keyAuth string) error { objectKey := strings.Trim(http01.ChallengePath(token), "/") if err := p.client.RemoveObject(context.Background(), p.bucket, objectKey); err != nil { return fmt.Errorf("s3: could not remove file in s3 bucket after HTTP challenge: %w", err) } return nil } func createS3Client(config ChallengerConfig) (*s3.Client, error) { clientCfg := s3.NewDefaultConfig() clientCfg.Endpoint = config.Endpoint clientCfg.AccessKey = config.AccessKey clientCfg.SecretKey = config.SecretKey clientCfg.SignatureVersion = config.SignatureVersion clientCfg.UsePathStyle = config.UsePathStyle clientCfg.Region = config.Region clientCfg.SkipTlsVerify = config.AllowInsecureConnections client, err := s3.NewClient(clientCfg) if err != nil { return nil, err } return client, err } ================================================ FILE: pkg/core/certifier/challengers/http01/ssh/ssh.go ================================================ package ssh import ( "errors" "fmt" "path/filepath" "github.com/go-acme/lego/v4/challenge/http01" "github.com/certimate-go/certimate/internal/tools/ssh" "github.com/certimate-go/certimate/pkg/core/certifier" xssh "github.com/certimate-go/certimate/pkg/utils/ssh" ) type ServerConfig struct { // SSH 主机。 // 零值时默认值 "localhost"。 SshHost string `json:"sshHost,omitempty"` // SSH 端口。 // 零值时默认值 22。 SshPort int32 `json:"sshPort,omitempty"` // SSH 认证方式。 // 可取值 "none"、"password"、"key"。 // 零值时根据有无密码或私钥字段决定。 SshAuthMethod string `json:"sshAuthMethod,omitempty"` // SSH 登录用户名。 // 零值时默认值 "root"。 SshUsername string `json:"sshUsername,omitempty"` // SSH 登录密码。 SshPassword string `json:"sshPassword,omitempty"` // SSH 登录私钥。 SshKey string `json:"sshKey,omitempty"` // SSH 登录私钥口令。 SshKeyPassphrase string `json:"sshKeyPassphrase,omitempty"` } type ChallengerConfig struct { ServerConfig // 跳板机配置数组。 JumpServers []ServerConfig `json:"jumpServers,omitempty"` // 是否回退使用 SCP。 UseSCP bool `json:"useSCP,omitempty"` // 网站根目录路径。 WebRootPath string `json:"webRootPath"` } func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { if config == nil { return nil, errors.New("the configuration of the acme challenge provider is nil") } provider := &provider{config: config} return provider, nil } type provider struct { config *ChallengerConfig } func (p *provider) Present(domain, token, keyAuth string) error { client, err := createSshClient(*p.config) if err != nil { return fmt.Errorf("ssh: failed to create SSH client: %w", err) } defer client.Close() challengeFilePath := filepath.Join(p.config.WebRootPath, http01.ChallengePath(token)) if err := xssh.WriteRemoteString(client.GetClient(), challengeFilePath, keyAuth, p.config.UseSCP); err != nil { return fmt.Errorf("failed to write file in webroot for HTTP challenge: %w", err) } return nil } func (p *provider) CleanUp(domain, token, keyAuth string) error { client, err := createSshClient(*p.config) if err != nil { return fmt.Errorf("ssh: failed to create SSH client: %w", err) } defer client.Close() // 删除质询文件 challengeFilePath := filepath.Join(p.config.WebRootPath, http01.ChallengePath(token)) xssh.RemoveRemote(client.GetClient(), challengeFilePath, p.config.UseSCP) return nil } func createSshClient(config ChallengerConfig) (*ssh.Client, error) { clientCfg := ssh.NewDefaultConfig() clientCfg.Host = config.SshHost clientCfg.Port = int(config.SshPort) clientCfg.AuthMethod = ssh.AuthMethodType(config.SshAuthMethod) clientCfg.Username = config.SshUsername clientCfg.Password = config.SshPassword clientCfg.Key = config.SshKey clientCfg.KeyPassphrase = config.SshKeyPassphrase for _, jumpServer := range config.JumpServers { jumpServerCfg := ssh.NewServerConfig() jumpServerCfg.Host = jumpServer.SshHost jumpServerCfg.Port = int(jumpServer.SshPort) jumpServerCfg.AuthMethod = ssh.AuthMethodType(jumpServer.SshAuthMethod) jumpServerCfg.Username = jumpServer.SshUsername jumpServerCfg.Password = jumpServer.SshPassword jumpServerCfg.Key = jumpServer.SshKey jumpServerCfg.KeyPassphrase = jumpServer.SshKeyPassphrase clientCfg.JumpServers = append(clientCfg.JumpServers, *jumpServerCfg) } client, err := ssh.NewClient(clientCfg) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/certmgr/errors.go ================================================ package certmgr import ( "errors" ) var ( ErrNotImplemented = errors.New("not implemented function") ErrUnsupported = errors.ErrUnsupported ) ================================================ FILE: pkg/core/certmgr/provider.go ================================================ package certmgr import ( "context" "log/slog" ) // 表示定义 SSL 证书管理器的抽象类型接口。 // 云服务商通常会提供 SSL 证书管理服务,可供用户集中管理证书。 type Provider interface { // 设置日志记录器。 // // 入参: // - logger:日志记录器实例。 SetLogger(logger *slog.Logger) // 上传证书。 // // 入参: // - ctx:上下文。 // - certPEM:证书 PEM 内容。 // - privkeyPEM:私钥 PEM 内容。 // // 出参: // - res:上传结果。 // - err: 错误。 Upload(ctx context.Context, certPEM, privkeyPEM string) (_res *UploadResult, _err error) // 更新证书。 // // 入参: // - ctx:上下文。 // - certIdOrName:证书 ID 或名称,即云服务商处的证书标识符。 // - certPEM:证书 PEM 内容。 // - privkeyPEM:私钥 PEM 内容。 // // 出参: // - res:操作结果。 // - err: 错误。 Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (_res *OperateResult, _err error) } // 表示 SSL 证书管理操作结果的数据结构。 type OperateResult struct { ExtendedData map[string]any `json:"extendedData,omitempty"` } // 表示 SSL 证书管理上传结果的数据结构,包含上传后的证书 ID、名称和其他数据。 type UploadResult struct { OperateResult CertId string `json:"certId,omitempty"` CertName string `json:"certName,omitempty"` ExtendedData map[string]any `json:"extendedData,omitempty"` } ================================================ FILE: pkg/core/certmgr/providers/1panel/1panel.go ================================================ package onepanelssl import ( "context" "crypto/tls" "errors" "fmt" "log/slog" "strconv" "strings" "time" "github.com/certimate-go/certimate/pkg/core/certmgr" onepanelsdk "github.com/certimate-go/certimate/pkg/sdk3rd/1panel" onepanelsdk2 "github.com/certimate-go/certimate/pkg/sdk3rd/1panel/v2" ) type CertmgrConfig struct { // 1Panel 服务地址。 ServerUrl string `json:"serverUrl"` // 1Panel 版本。 ApiVersion string `json:"apiVersion"` // 1Panel 接口密钥。 ApiKey string `json:"apiKey"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` // 子节点名称。 // 选填。 NodeName string `json:"nodeName,omitempty"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient any } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.ServerUrl, config.ApiVersion, config.ApiKey, config.AllowInsecureConnections, config.NodeName) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 避免重复上传 if upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM, privkeyPEM); err != nil { return nil, err } else if upok { c.logger.Info("ssl certificate already exists") return upres, nil } // 生成新证书名(需符合 1Panel 命名规则) certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) // 上传证书 switch sdkClient := c.sdkClient.(type) { case *onepanelsdk.Client: { websiteSSLUploadReq := &onepanelsdk.WebsiteSSLUploadRequest{ Type: "paste", Description: certName, Certificate: certPEM, PrivateKey: privkeyPEM, } websiteSSLUploadResp, err := sdkClient.WebsiteSSLUploadWithContext(ctx, websiteSSLUploadReq) c.logger.Debug("sdk request '1panel.WebsiteSSLUpload'", slog.Any("request", websiteSSLUploadReq), slog.Any("response", websiteSSLUploadResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request '1panel.WebsiteSSLUpload': %w", err) } } case *onepanelsdk2.Client: { websiteSSLUploadReq := &onepanelsdk2.WebsiteSSLUploadRequest{ Type: "paste", Description: certName, Certificate: certPEM, PrivateKey: privkeyPEM, } websiteSSLUploadResp, err := sdkClient.WebsiteSSLUploadWithContext(ctx, websiteSSLUploadReq) c.logger.Debug("sdk request '1panel.WebsiteSSLUpload'", slog.Any("request", websiteSSLUploadReq), slog.Any("response", websiteSSLUploadResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request '1panel.WebsiteSSLUpload': %w", err) } } default: panic("unreachable") } // 获取刚刚上传证书 ID if upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM, privkeyPEM); err != nil { return nil, err } else if !upok { return nil, fmt.Errorf("could not find ssl certificate, may be upload failed") } else { return upres, nil } } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { sslId, err := strconv.ParseInt(certIdOrName, 10, 64) if err != nil { return nil, err } switch sdkClient := c.sdkClient.(type) { case *onepanelsdk.Client: { // 获取证书详情 websiteSSLGetResp, err := sdkClient.WebsiteSSLGetWithContext(ctx, sslId) c.logger.Debug("sdk request '1panel.WebsiteSSLGet'", slog.Int64("sslId", sslId), slog.Any("response", websiteSSLGetResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request '1panel.WebsiteSSLGet': %w", err) } // 更新证书 websiteSSLUploadReq := &onepanelsdk.WebsiteSSLUploadRequest{ SSLID: sslId, Type: "paste", Description: websiteSSLGetResp.Data.Description, Certificate: certPEM, PrivateKey: privkeyPEM, } websiteSSLUploadResp, err := sdkClient.WebsiteSSLUploadWithContext(ctx, websiteSSLUploadReq) c.logger.Debug("sdk request '1panel.WebsiteSSLUpload'", slog.Any("request", websiteSSLUploadReq), slog.Any("response", websiteSSLUploadResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request '1panel.WebsiteSSLUpload': %w", err) } } case *onepanelsdk2.Client: { // 获取证书详情 websiteSSLGetResp, err := sdkClient.WebsiteSSLGetWithContext(ctx, sslId) c.logger.Debug("sdk request '1panel.WebsiteSSLGet'", slog.Any("sslId", sslId), slog.Any("response", websiteSSLGetResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request '1panel.WebsiteSSLGet': %w", err) } // 更新证书 websiteSSLUploadReq := &onepanelsdk2.WebsiteSSLUploadRequest{ SSLID: sslId, Type: "paste", Description: websiteSSLGetResp.Data.Description, Certificate: certPEM, PrivateKey: privkeyPEM, } websiteSSLUploadResp, err := sdkClient.WebsiteSSLUploadWithContext(ctx, websiteSSLUploadReq) c.logger.Debug("sdk request '1panel.WebsiteSSLUpload'", slog.Any("request", websiteSSLUploadReq), slog.Any("response", websiteSSLUploadResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request '1panel.WebsiteSSLUpload': %w", err) } } default: panic("unreachable") } return &certmgr.OperateResult{}, nil } func (c *Certmgr) tryGetResultIfCertExists(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, bool, error) { switch sdkClient := c.sdkClient.(type) { case *onepanelsdk.Client: { searchWebsiteSSLPage := 1 searchWebsiteSSLPageSize := 100 for { select { case <-ctx.Done(): return nil, false, ctx.Err() default: } websiteSSLSearchReq := &onepanelsdk.WebsiteSSLSearchRequest{ Page: int32(searchWebsiteSSLPage), PageSize: int32(searchWebsiteSSLPageSize), } websiteSSLSearchResp, err := sdkClient.WebsiteSSLSearchWithContext(ctx, websiteSSLSearchReq) c.logger.Debug("sdk request '1panel.WebsiteSSLSearch'", slog.Any("request", websiteSSLSearchReq), slog.Any("response", websiteSSLSearchResp)) if err != nil { return nil, false, fmt.Errorf("failed to execute sdk request '1panel.WebsiteSSLSearch': %w", err) } if websiteSSLSearchResp.Data == nil { break } for _, sslItem := range websiteSSLSearchResp.Data.Items { oldCertPEM := strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(sslItem.PEM, "\r", ""), "\n", "")) oldPrivkeyPEM := strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(sslItem.PrivateKey, "\r", ""), "\n", "")) newCertPEM := strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(certPEM, "\r", ""), "\n", "")) newPrivkeyPEM := strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(privkeyPEM, "\r", ""), "\n", "")) if oldCertPEM == newCertPEM && oldPrivkeyPEM == newPrivkeyPEM { // 如果已存在相同证书,直接返回 return &certmgr.UploadResult{ CertId: fmt.Sprintf("%d", sslItem.ID), CertName: sslItem.Description, }, true, nil } } if len(websiteSSLSearchResp.Data.Items) < int(websiteSSLSearchResp.Data.Total) { break } searchWebsiteSSLPage++ } } case *onepanelsdk2.Client: { searchWebsiteSSLPage := 1 searchWebsiteSSLPageSize := 100 for { select { case <-ctx.Done(): return nil, false, ctx.Err() default: } websiteSSLSearchReq := &onepanelsdk2.WebsiteSSLSearchRequest{ Order: "null", OrderBy: "expire_date", Page: int32(searchWebsiteSSLPage), PageSize: int32(searchWebsiteSSLPageSize), } websiteSSLSearchResp, err := sdkClient.WebsiteSSLSearchWithContext(ctx, websiteSSLSearchReq) c.logger.Debug("sdk request '1panel.WebsiteSSLSearch'", slog.Any("request", websiteSSLSearchReq), slog.Any("response", websiteSSLSearchResp)) if err != nil { return nil, false, fmt.Errorf("failed to execute sdk request '1panel.WebsiteSSLSearch': %w", err) } if websiteSSLSearchResp.Data == nil { break } for _, sslItem := range websiteSSLSearchResp.Data.Items { oldCertPEM := strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(sslItem.PEM, "\r", ""), "\n", "")) oldPrivkeyPEM := strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(sslItem.PrivateKey, "\r", ""), "\n", "")) newCertPEM := strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(certPEM, "\r", ""), "\n", "")) newPrivkeyPEM := strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(privkeyPEM, "\r", ""), "\n", "")) if oldCertPEM == newCertPEM && oldPrivkeyPEM == newPrivkeyPEM { // 如果已存在相同证书,直接返回 return &certmgr.UploadResult{ CertId: fmt.Sprintf("%d", sslItem.ID), CertName: sslItem.Description, }, true, nil } } if len(websiteSSLSearchResp.Data.Items) < int(websiteSSLSearchResp.Data.Total) { break } searchWebsiteSSLPage++ } } default: panic("unreachable") } return nil, false, nil } const ( sdkVersionV1 = "v1" sdkVersionV2 = "v2" ) func createSDKClient(serverUrl, apiVersion, apiKey string, skipTlsVerify bool, nodeName string) (any, error) { if apiVersion == sdkVersionV1 { client, err := onepanelsdk.NewClient(serverUrl, apiKey) if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } else if apiVersion == sdkVersionV2 { var client *onepanelsdk2.Client var err error if nodeName == "" { client, err = onepanelsdk2.NewClient(serverUrl, apiKey) } else { client, err = onepanelsdk2.NewClientWithNode(serverUrl, apiKey, nodeName) } if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } return nil, errors.New("1panel: invalid api version") } ================================================ FILE: pkg/core/certmgr/providers/1panel/1panel_test.go ================================================ package onepanelssl_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/1panel" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fApiVersion string fApiKey string ) func init() { argsPrefix := "1PANEL_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fApiVersion, argsPrefix+"APIVERSION", "v1", "") flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") } /* Shell command to run this test: go test -v ./1panel_test.go -args \ --1PANEL_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --1PANEL_INPUTKEYPATH="/path/to/your-input-key.pem" \ --1PANEL_SERVERURL="http://127.0.0.1:20410" \ --1PANEL_APIVERSION="v1" \ --1PANEL_APIKEY="your-api-key" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("APIVERSION: %v", fApiVersion), fmt.Sprintf("APIKEY: %v", fApiKey), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ ServerUrl: fServerUrl, ApiVersion: fApiVersion, ApiKey: fApiKey, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/certmgr/providers/aliyun-cas/aliyun_cas.go ================================================ package aliyuncas import ( "context" "errors" "fmt" "log/slog" "strings" "time" alicas "github.com/alibabacloud-go/cas-20200407/v4/client" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" "github.com/alibabacloud-go/tea/dara" "github.com/alibabacloud-go/tea/tea" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // 阿里云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 阿里云资源组 ID。 ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *internal.CasClient } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } // 查询证书列表,避免重复上传 // REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-listusercertificateorder // REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-getusercertificatedetail listUserCertificateOrderPage := 1 listUserCertificateOrderLimit := 50 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } listUserCertificateOrderReq := &alicas.ListUserCertificateOrderRequest{ ResourceGroupId: lo.EmptyableToPtr(c.config.ResourceGroupId), CurrentPage: tea.Int64(int64(listUserCertificateOrderPage)), ShowSize: tea.Int64(int64(listUserCertificateOrderLimit)), OrderType: tea.String("CERT"), } listUserCertificateOrderResp, err := c.sdkClient.ListUserCertificateOrderWithContext(ctx, listUserCertificateOrderReq, &dara.RuntimeOptions{}) c.logger.Debug("sdk request 'cas.ListUserCertificateOrder'", slog.Any("request", listUserCertificateOrderReq), slog.Any("response", listUserCertificateOrderResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cas.ListUserCertificateOrder': %w", err) } if listUserCertificateOrderResp.Body == nil { break } for _, certItem := range listUserCertificateOrderResp.Body.CertificateOrderList { // 对比证书通用名称 if !strings.EqualFold(certX509.Subject.CommonName, tea.StringValue(certItem.CommonName)) { continue } // 对比证书序列号 // 注意阿里云 CAS 会在序列号前补零,需去除后再比较 oldCertSN := strings.TrimLeft(tea.StringValue(certItem.SerialNo), "0") newCertSN := strings.TrimLeft(certX509.SerialNumber.Text(16), "0") if !strings.EqualFold(newCertSN, oldCertSN) { continue } // 对比证书内容 getUserCertificateDetailReq := &alicas.GetUserCertificateDetailRequest{ CertId: certItem.CertificateId, } getUserCertificateDetailResp, err := c.sdkClient.GetUserCertificateDetailWithContext(ctx, getUserCertificateDetailReq, &dara.RuntimeOptions{}) c.logger.Debug("sdk request 'cas.GetUserCertificateDetail'", slog.Any("request", getUserCertificateDetailReq), slog.Any("response", getUserCertificateDetailResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cas.GetUserCertificateDetail': %w", err) } else { if !xcert.EqualCertificatesFromPEM(certPEM, tea.StringValue(getUserCertificateDetailResp.Body.Cert)) { continue } } // 如果以上信息都一致,则视为已存在相同证书,直接返回 c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: fmt.Sprintf("%d", tea.Int64Value(certItem.CertificateId)), CertName: *certItem.Name, ExtendedData: map[string]any{ "InstanceId": tea.StringValue(getUserCertificateDetailResp.Body.InstanceId), "CertIdentifier": tea.StringValue(getUserCertificateDetailResp.Body.CertIdentifier), }, }, nil } if len(listUserCertificateOrderResp.Body.CertificateOrderList) < listUserCertificateOrderLimit { break } listUserCertificateOrderPage++ } // 生成新证书名(需符合阿里云命名规则) certName := fmt.Sprintf("certimate_%d", time.Now().UnixMilli()) // 上传新证书 // REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-uploadusercertificate uploadUserCertificateReq := &alicas.UploadUserCertificateRequest{ ResourceGroupId: lo.EmptyableToPtr(c.config.ResourceGroupId), Name: tea.String(certName), Cert: tea.String(certPEM), Key: tea.String(privkeyPEM), } uploadUserCertificateResp, err := c.sdkClient.UploadUserCertificateWithContext(ctx, uploadUserCertificateReq, &dara.RuntimeOptions{}) c.logger.Debug("sdk request 'cas.UploadUserCertificate'", slog.Any("request", uploadUserCertificateReq), slog.Any("response", uploadUserCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cas.UploadUserCertificate': %w", err) } // 获取证书详情 // REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-getusercertificatedetail getUserCertificateDetailReq := &alicas.GetUserCertificateDetailRequest{ CertId: uploadUserCertificateResp.Body.CertId, CertFilter: tea.Bool(true), } getUserCertificateDetailResp, err := c.sdkClient.GetUserCertificateDetailWithContext(ctx, getUserCertificateDetailReq, &dara.RuntimeOptions{}) c.logger.Debug("sdk request 'cas.GetUserCertificateDetail'", slog.Any("request", getUserCertificateDetailReq), slog.Any("response", getUserCertificateDetailResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cas.GetUserCertificateDetail': %w", err) } return &certmgr.UploadResult{ CertId: fmt.Sprintf("%d", tea.Int64Value(getUserCertificateDetailResp.Body.Id)), CertName: certName, ExtendedData: map[string]any{ "InstanceId": tea.StringValue(getUserCertificateDetailResp.Body.InstanceId), "CertIdentifier": tea.StringValue(getUserCertificateDetailResp.Body.CertIdentifier), }, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.CasClient, error) { // 接入点一览 https://api.aliyun.com/product/cas var endpoint string switch region { case "", "cn-hangzhou": endpoint = "cas.aliyuncs.com" default: endpoint = fmt.Sprintf("cas.%s.aliyuncs.com", region) } config := &aliopen.Config{ Endpoint: tea.String(endpoint), AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), } client, err := internal.NewCasClient(config) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/certmgr/providers/aliyun-cas/aliyun_cas_test.go ================================================ package aliyuncas_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fRegion string ) func init() { argsPrefix := "ALIYUNCAS_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") } /* Shell command to run this test: go test -v ./aliyun_cas_test.go -args \ --ALIYUNCAS_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --ALIYUNCAS_INPUTKEYPATH="/path/to/your-input-key.pem" \ --ALIYUNCAS_ACCESSKEYID="your-access-key-id" \ --ALIYUNCAS_ACCESSKEYSECRET="your-access-key-secret" \ --ALIYUNCAS_REGION="cn-hangzhou" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGION: %v", fRegion), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Region: fRegion, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/certmgr/providers/aliyun-cas/internal/client.go ================================================ package internal import ( "context" alicas "github.com/alibabacloud-go/cas-20200407/v4/client" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" openapiutil "github.com/alibabacloud-go/darabonba-openapi/v2/utils" "github.com/alibabacloud-go/tea/dara" ) // This is a partial copy of https://github.com/alibabacloud-go/cas-20200407/blob/master/client/client_context_func.go // to lightweight the vendor packages in the built binary. type CasClient struct { openapi.Client DisableSDKError *bool } func NewCasClient(config *openapiutil.Config) (*CasClient, error) { client := new(CasClient) err := client.Init(config) return client, err } func (client *CasClient) Init(config *openapiutil.Config) (_err error) { _err = client.Client.Init(config) if _err != nil { return _err } _err = client.CheckConfig(config) if _err != nil { return _err } return nil } func (client *CasClient) GetUserCertificateDetailWithContext(ctx context.Context, request *alicas.GetUserCertificateDetailRequest, runtime *dara.RuntimeOptions) (_result *alicas.GetUserCertificateDetailResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.CertFilter) { query["CertFilter"] = request.CertFilter } if !dara.IsNil(request.CertId) { query["CertId"] = request.CertId } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("GetUserCertificateDetail"), Version: dara.String("2020-04-07"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alicas.GetUserCertificateDetailResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *CasClient) ListUserCertificateOrderWithContext(ctx context.Context, request *alicas.ListUserCertificateOrderRequest, runtime *dara.RuntimeOptions) (_result *alicas.ListUserCertificateOrderResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.CurrentPage) { query["CurrentPage"] = request.CurrentPage } if !dara.IsNil(request.Keyword) { query["Keyword"] = request.Keyword } if !dara.IsNil(request.OrderType) { query["OrderType"] = request.OrderType } if !dara.IsNil(request.ResourceGroupId) { query["ResourceGroupId"] = request.ResourceGroupId } if !dara.IsNil(request.ShowSize) { query["ShowSize"] = request.ShowSize } if !dara.IsNil(request.Status) { query["Status"] = request.Status } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("ListUserCertificateOrder"), Version: dara.String("2020-04-07"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alicas.ListUserCertificateOrderResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *CasClient) UploadUserCertificateWithContext(ctx context.Context, request *alicas.UploadUserCertificateRequest, runtime *dara.RuntimeOptions) (_result *alicas.UploadUserCertificateResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.Cert) { query["Cert"] = request.Cert } if !dara.IsNil(request.EncryptCert) { query["EncryptCert"] = request.EncryptCert } if !dara.IsNil(request.EncryptPrivateKey) { query["EncryptPrivateKey"] = request.EncryptPrivateKey } if !dara.IsNil(request.Key) { query["Key"] = request.Key } if !dara.IsNil(request.Name) { query["Name"] = request.Name } if !dara.IsNil(request.ResourceGroupId) { query["ResourceGroupId"] = request.ResourceGroupId } if !dara.IsNil(request.SignCert) { query["SignCert"] = request.SignCert } if !dara.IsNil(request.SignPrivateKey) { query["SignPrivateKey"] = request.SignPrivateKey } if !dara.IsNil(request.Tags) { query["Tags"] = request.Tags } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("UploadUserCertificate"), Version: dara.String("2020-04-07"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alicas.UploadUserCertificateResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } ================================================ FILE: pkg/core/certmgr/providers/aliyun-slb/aliyun_slb.go ================================================ package aliyunslb import ( "context" "crypto/sha1" "crypto/sha256" "encoding/hex" "errors" "fmt" "log/slog" "regexp" "strings" "time" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" alislb "github.com/alibabacloud-go/slb-20140515/v4/client" "github.com/alibabacloud-go/tea/dara" "github.com/alibabacloud-go/tea/tea" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-slb/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // 阿里云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 阿里云资源组 ID。 ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *internal.SlbClient } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } // 查询证书列表,避免重复上传 // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeservercertificates describeServerCertificatesReq := &alislb.DescribeServerCertificatesRequest{ ResourceGroupId: lo.EmptyableToPtr(c.config.ResourceGroupId), RegionId: tea.String(c.config.Region), } describeServerCertificatesResp, err := c.sdkClient.DescribeServerCertificatesWithContext(ctx, describeServerCertificatesReq, &dara.RuntimeOptions{}) c.logger.Debug("sdk request 'slb.DescribeServerCertificates'", slog.Any("request", describeServerCertificatesReq), slog.Any("response", describeServerCertificatesResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'slb.DescribeServerCertificates': %w", err) } if describeServerCertificatesResp.Body.ServerCertificates != nil && describeServerCertificatesResp.Body.ServerCertificates.ServerCertificate != nil { fingerprintSha256 := sha256.Sum256(certX509.Raw) fingerprintSha256Hex := hex.EncodeToString(fingerprintSha256[:]) fingerprintSha1 := sha1.Sum(certX509.Raw) fingerprintSha1Hex := hex.EncodeToString(fingerprintSha1[:]) for _, certItem := range describeServerCertificatesResp.Body.ServerCertificates.ServerCertificate { if tea.Int32Value(certItem.IsAliCloudCertificate) != 0 { continue } // 对比证书通用名称 if !strings.EqualFold(certX509.Subject.CommonName, tea.StringValue(certItem.CommonName)) { continue } // 对比证书 SHA-1 或 SHA-256 摘要 oldFingerprint := strings.ReplaceAll(tea.StringValue(certItem.Fingerprint), ":", "") if !strings.EqualFold(fingerprintSha256Hex, oldFingerprint) && !strings.EqualFold(fingerprintSha1Hex, oldFingerprint) { continue } // 如果以上信息都一致,则视为已存在相同证书,直接返回 c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: *certItem.ServerCertificateId, CertName: *certItem.ServerCertificateName, }, nil } } // 生成新证书名(需符合阿里云命名规则) certName := fmt.Sprintf("certimate_%d", time.Now().UnixMilli()) // 去除证书和私钥内容中的空白行,以符合阿里云 API 要求 // REF: https://github.com/certimate-go/certimate/issues/326 re := regexp.MustCompile(`(?m)^\s*$\n?`) certPEM = strings.TrimSpace(re.ReplaceAllString(certPEM, "")) privkeyPEM = strings.TrimSpace(re.ReplaceAllString(privkeyPEM, "")) // 上传新证书 // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-uploadservercertificate uploadServerCertificateReq := &alislb.UploadServerCertificateRequest{ ResourceGroupId: lo.EmptyableToPtr(c.config.ResourceGroupId), RegionId: tea.String(c.config.Region), ServerCertificateName: tea.String(certName), ServerCertificate: tea.String(certPEM), PrivateKey: tea.String(privkeyPEM), } uploadServerCertificateResp, err := c.sdkClient.UploadServerCertificateWithContext(ctx, uploadServerCertificateReq, &dara.RuntimeOptions{}) c.logger.Debug("sdk request 'slb.UploadServerCertificate'", slog.Any("request", uploadServerCertificateReq), slog.Any("response", uploadServerCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'slb.UploadServerCertificate': %w", err) } return &certmgr.UploadResult{ CertId: *uploadServerCertificateResp.Body.ServerCertificateId, CertName: certName, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.SlbClient, error) { // 接入点一览 https://api.aliyun.com/product/Slb var endpoint string switch region { case "", "cn-hangzhou", "cn-hangzhou-finance", "cn-shanghai-finance-1", "cn-shenzhen-finance-1": endpoint = "slb.aliyuncs.com" default: endpoint = fmt.Sprintf("slb.%s.aliyuncs.com", region) } config := &aliopen.Config{ Endpoint: tea.String(endpoint), AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), } client, err := internal.NewSlbClient(config) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/certmgr/providers/aliyun-slb/aliyun_slb_test.go ================================================ package aliyunslb_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-slb" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fRegion string ) func init() { argsPrefix := "ALIYUNSLB_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") } /* Shell command to run this test: go test -v ./aliyun_slb_test.go -args \ --ALIYUNSLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --ALIYUNSLB_INPUTKEYPATH="/path/to/your-input-key.pem" \ --ALIYUNSLB_ACCESSKEYID="your-access-key-id" \ --ALIYUNSLB_ACCESSKEYSECRET="your-access-key-secret" \ --ALIYUNSLB_REGION="cn-hangzhou" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGION: %v", fRegion), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Region: fRegion, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/certmgr/providers/aliyun-slb/internal/client.go ================================================ package internal import ( "context" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" openapiutil "github.com/alibabacloud-go/darabonba-openapi/v2/utils" alislb "github.com/alibabacloud-go/slb-20140515/v4/client" "github.com/alibabacloud-go/tea/dara" ) // This is a partial copy of https://github.com/alibabacloud-go/slb-20140515/blob/master/client/client_context_func.go // to lightweight the vendor packages in the built binary. type SlbClient struct { openapi.Client DisableSDKError *bool } func NewSlbClient(config *openapi.Config) (*SlbClient, error) { client := new(SlbClient) err := client.Init(config) return client, err } func (client *SlbClient) Init(config *openapi.Config) (_err error) { _err = client.Client.Init(config) if _err != nil { return _err } _err = client.CheckConfig(config) if _err != nil { return _err } return nil } func (client *SlbClient) DescribeServerCertificatesWithContext(ctx context.Context, request *alislb.DescribeServerCertificatesRequest, runtime *dara.RuntimeOptions) (_result *alislb.DescribeServerCertificatesResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.OwnerAccount) { query["OwnerAccount"] = request.OwnerAccount } if !dara.IsNil(request.OwnerId) { query["OwnerId"] = request.OwnerId } if !dara.IsNil(request.RegionId) { query["RegionId"] = request.RegionId } if !dara.IsNil(request.ResourceGroupId) { query["ResourceGroupId"] = request.ResourceGroupId } if !dara.IsNil(request.ResourceOwnerAccount) { query["ResourceOwnerAccount"] = request.ResourceOwnerAccount } if !dara.IsNil(request.ResourceOwnerId) { query["ResourceOwnerId"] = request.ResourceOwnerId } if !dara.IsNil(request.ServerCertificateId) { query["ServerCertificateId"] = request.ServerCertificateId } if !dara.IsNil(request.Tag) { query["Tag"] = request.Tag } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("DescribeServerCertificates"), Version: dara.String("2014-05-15"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alislb.DescribeServerCertificatesResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *SlbClient) UploadServerCertificateWithContext(ctx context.Context, request *alislb.UploadServerCertificateRequest, runtime *dara.RuntimeOptions) (_result *alislb.UploadServerCertificateResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.AliCloudCertificateId) { query["AliCloudCertificateId"] = request.AliCloudCertificateId } if !dara.IsNil(request.AliCloudCertificateName) { query["AliCloudCertificateName"] = request.AliCloudCertificateName } if !dara.IsNil(request.AliCloudCertificateRegionId) { query["AliCloudCertificateRegionId"] = request.AliCloudCertificateRegionId } if !dara.IsNil(request.OwnerAccount) { query["OwnerAccount"] = request.OwnerAccount } if !dara.IsNil(request.OwnerId) { query["OwnerId"] = request.OwnerId } if !dara.IsNil(request.PrivateKey) { query["PrivateKey"] = request.PrivateKey } if !dara.IsNil(request.RegionId) { query["RegionId"] = request.RegionId } if !dara.IsNil(request.ResourceGroupId) { query["ResourceGroupId"] = request.ResourceGroupId } if !dara.IsNil(request.ResourceOwnerAccount) { query["ResourceOwnerAccount"] = request.ResourceOwnerAccount } if !dara.IsNil(request.ResourceOwnerId) { query["ResourceOwnerId"] = request.ResourceOwnerId } if !dara.IsNil(request.ServerCertificate) { query["ServerCertificate"] = request.ServerCertificate } if !dara.IsNil(request.ServerCertificateName) { query["ServerCertificateName"] = request.ServerCertificateName } if !dara.IsNil(request.Tag) { query["Tag"] = request.Tag } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("UploadServerCertificate"), Version: dara.String("2014-05-15"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alislb.UploadServerCertificateResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } ================================================ FILE: pkg/core/certmgr/providers/aws-acm/aws_acm.go ================================================ package awsacm import ( "context" "errors" "fmt" "log/slog" "strings" aws "github.com/aws/aws-sdk-go-v2/aws" awscfg "github.com/aws/aws-sdk-go-v2/config" awscred "github.com/aws/aws-sdk-go-v2/credentials" awsacm "github.com/aws/aws-sdk-go-v2/service/acm" "github.com/certimate-go/certimate/pkg/core/certmgr" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // AWS AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // AWS SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // AWS 区域。 Region string `json:"region"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *awsacm.Client } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } // 提取服务器证书和中间证书 serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM) if err != nil { return nil, fmt.Errorf("failed to extract certs: %w", err) } // 获取证书列表,避免重复上传 // REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ListCertificates.html // REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_GetCertificate.html listCertificatesNextToken := (*string)(nil) for { select { case <-ctx.Done(): return nil, ctx.Err() default: } listCertificatesReq := &awsacm.ListCertificatesInput{ NextToken: listCertificatesNextToken, MaxItems: aws.Int32(1000), } listCertificatesResp, err := c.sdkClient.ListCertificates(ctx, listCertificatesReq) c.logger.Debug("sdk request 'acm.ListCertificates'", slog.Any("request", listCertificatesReq), slog.Any("response", listCertificatesResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'acm.ListCertificates': %w", err) } for _, certItem := range listCertificatesResp.CertificateSummaryList { // 对比证书有效期 if certItem.NotBefore == nil || !certItem.NotBefore.Equal(certX509.NotBefore) { continue } if certItem.NotAfter == nil || !certItem.NotAfter.Equal(certX509.NotAfter) { continue } // 对比证书多域名 if !strings.EqualFold(strings.Join(certX509.DNSNames, ","), strings.Join(certItem.SubjectAlternativeNameSummaries, ",")) { continue } // 对比证书内容 getCertificateReq := &awsacm.GetCertificateInput{ CertificateArn: certItem.CertificateArn, } getCertificateResp, err := c.sdkClient.GetCertificate(ctx, getCertificateReq) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'acm.GetCertificate': %w", err) } else { if !xcert.EqualCertificatesFromPEM(certPEM, aws.ToString(getCertificateResp.Certificate)) { continue } } // 如果以上信息都一致,则视为已存在相同证书,直接返回 c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: *certItem.CertificateArn, }, nil } if len(listCertificatesResp.CertificateSummaryList) == 0 || listCertificatesResp.NextToken == nil { break } listCertificatesNextToken = listCertificatesResp.NextToken } // 导入证书 // REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ImportCertificate.html importCertificateReq := &awsacm.ImportCertificateInput{ Certificate: ([]byte)(serverCertPEM), CertificateChain: ([]byte)(intermediaCertPEM), PrivateKey: ([]byte)(privkeyPEM), } importCertificateResp, err := c.sdkClient.ImportCertificate(ctx, importCertificateReq) c.logger.Debug("sdk request 'acm.ImportCertificate'", slog.Any("request", importCertificateReq), slog.Any("response", importCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'acm.ImportCertificate': %w", err) } return &certmgr.UploadResult{ CertId: aws.ToString(importCertificateResp.CertificateArn), }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { // 提取服务器证书和中间证书 serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM) if err != nil { return nil, fmt.Errorf("failed to extract certs: %w", err) } // 导入证书 // REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ImportCertificate.html importCertificateReq := &awsacm.ImportCertificateInput{ CertificateArn: aws.String(certIdOrName), Certificate: ([]byte)(serverCertPEM), CertificateChain: ([]byte)(intermediaCertPEM), PrivateKey: ([]byte)(privkeyPEM), } importCertificateResp, err := c.sdkClient.ImportCertificate(ctx, importCertificateReq) c.logger.Debug("sdk request 'acm.ImportCertificate'", slog.Any("request", importCertificateReq), slog.Any("response", importCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'acm.ImportCertificate': %w", err) } return &certmgr.OperateResult{}, nil } func createSDKClient(accessKeyId, secretAccessKey, region string) (*awsacm.Client, error) { cfg, err := awscfg.LoadDefaultConfig(context.Background()) if err != nil { return nil, err } client := awsacm.NewFromConfig(cfg, func(o *awsacm.Options) { o.Region = region o.Credentials = aws.NewCredentialsCache(awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, "")) }) return client, nil } ================================================ FILE: pkg/core/certmgr/providers/aws-iam/aws_iam.go ================================================ package awsiam import ( "context" "errors" "fmt" "log/slog" "time" aws "github.com/aws/aws-sdk-go-v2/aws" awscfg "github.com/aws/aws-sdk-go-v2/config" awscred "github.com/aws/aws-sdk-go-v2/credentials" awsiam "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/certimate-go/certimate/pkg/core/certmgr" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // AWS AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // AWS SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // AWS 区域。 Region string `json:"region"` // IAM 证书路径。 // 选填。 CertificatePath string `json:"certificatePath,omitempty"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *awsiam.Client } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } // 提取服务器证书和中间证书 serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM) if err != nil { return nil, fmt.Errorf("failed to extract certs: %w", err) } // 获取证书列表,避免重复上传 // REF: https://docs.aws.amazon.com/en_us/IAM/latest/APIReference/API_ListServerCertificates.html // REF: https://docs.aws.amazon.com/en_us/IAM/latest/APIReference/API_GetServerCertificate.html listServerCertificatesMarker := (*string)(nil) for { select { case <-ctx.Done(): return nil, ctx.Err() default: } listServerCertificatesReq := &awsiam.ListServerCertificatesInput{ Marker: listServerCertificatesMarker, MaxItems: aws.Int32(1000), } if c.config.CertificatePath != "" { listServerCertificatesReq.PathPrefix = aws.String(c.config.CertificatePath) } listServerCertificatesResp, err := c.sdkClient.ListServerCertificates(ctx, listServerCertificatesReq) c.logger.Debug("sdk request 'iam.ListServerCertificates'", slog.Any("request", listServerCertificatesReq), slog.Any("response", listServerCertificatesResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'iam.ListServerCertificates': %w", err) } for _, certItem := range listServerCertificatesResp.ServerCertificateMetadataList { // 对比证书路径 if c.config.CertificatePath != "" && aws.ToString(certItem.Path) != c.config.CertificatePath { continue } // 对比证书有效期 if certItem.Expiration == nil || !certItem.Expiration.Equal(certX509.NotAfter) { continue } // 对比证书内容 getServerCertificateReq := &awsiam.GetServerCertificateInput{ ServerCertificateName: certItem.ServerCertificateName, } getServerCertificateResp, err := c.sdkClient.GetServerCertificate(ctx, getServerCertificateReq) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'iam.GetServerCertificate': %w", err) } else { if !xcert.EqualCertificatesFromPEM(certPEM, aws.ToString(getServerCertificateResp.ServerCertificate.CertificateBody)) { continue } } // 如果以上信息都一致,则视为已存在相同证书,直接返回 c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: aws.ToString(certItem.ServerCertificateId), CertName: aws.ToString(certItem.ServerCertificateName), }, nil } if len(listServerCertificatesResp.ServerCertificateMetadataList) == 0 || listServerCertificatesResp.Marker == nil { break } listServerCertificatesMarker = listServerCertificatesResp.Marker } // 生成新证书名(需符合 AWS IAM 命名规则) certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) // 导入证书 // REF: https://docs.aws.amazon.com/en_us/IAM/latest/APIReference/API_UploadServerCertificate.html uploadServerCertificateReq := &awsiam.UploadServerCertificateInput{ ServerCertificateName: aws.String(certName), Path: aws.String(c.config.CertificatePath), CertificateBody: aws.String(serverCertPEM), CertificateChain: aws.String(intermediaCertPEM), PrivateKey: aws.String(privkeyPEM), } if c.config.CertificatePath == "" { uploadServerCertificateReq.Path = aws.String("/") } uploadServerCertificateResp, err := c.sdkClient.UploadServerCertificate(ctx, uploadServerCertificateReq) c.logger.Debug("sdk request 'iam.UploadServerCertificate'", slog.Any("request", uploadServerCertificateReq), slog.Any("response", uploadServerCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'iam.UploadServerCertificate': %w", err) } return &certmgr.UploadResult{ CertId: aws.ToString(uploadServerCertificateResp.ServerCertificateMetadata.ServerCertificateId), CertName: certName, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func createSDKClient(accessKeyId, secretAccessKey, region string) (*awsiam.Client, error) { cfg, err := awscfg.LoadDefaultConfig(context.Background()) if err != nil { return nil, err } client := awsiam.NewFromConfig(cfg, func(o *awsiam.Options) { o.Region = region o.Credentials = aws.NewCredentialsCache(awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, "")) }) return client, nil } ================================================ FILE: pkg/core/certmgr/providers/azure-keyvault/azure_keyvault.go ================================================ package azurekeyvault import ( "context" "encoding/base64" "errors" "fmt" "log/slog" "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates" "github.com/certimate-go/certimate/pkg/core/certmgr" azenv "github.com/certimate-go/certimate/pkg/sdk3rd/azure/env" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // Azure TenantId。 TenantId string `json:"tenantId"` // Azure ClientId。 ClientId string `json:"clientId"` // Azure ClientSecret。 ClientSecret string `json:"clientSecret"` // Azure 主权云环境。 CloudName string `json:"cloudName,omitempty"` // Key Vault 名称。 KeyVaultName string `json:"keyvaultName"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *azcertificates.Client } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.CloudName, config.TenantId, config.ClientId, config.ClientSecret, config.KeyVaultName) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } // 生成 Azure 业务参数 certCN := certX509.Subject.CommonName certSN := certX509.SerialNumber.Text(16) // 获取证书列表,避免重复上传 // REF: https://learn.microsoft.com/en-us/rest/api/keyvault/certificates/get-certificates/get-certificates listCertificatesPager := c.sdkClient.NewListCertificatePropertiesPager(&azcertificates.ListCertificatePropertiesOptions{}) for listCertificatesPager.More() { page, err := listCertificatesPager.NextPage(ctx) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'keyvault.GetCertificates': %w", err) } for _, certItem := range page.Value { // 对比证书有效期 if certItem.Attributes == nil { continue } if certItem.Attributes.NotBefore == nil || !certItem.Attributes.NotBefore.Equal(certX509.NotBefore) { continue } if certItem.Attributes.Expires == nil || !certItem.Attributes.Expires.Equal(certX509.NotAfter) { continue } // 对比 Tag 中的通用名称 if v, ok := certItem.Tags[kvTagCertCN]; !ok || v == nil { continue } else if *v != certCN { continue } // 对比 Tag 中的序列号 if v, ok := certItem.Tags[kvTagCertSN]; !ok || v == nil { continue } else if *v != certSN { continue } // 对比证书内容 getCertificateResp, err := c.sdkClient.GetCertificate(ctx, certItem.ID.Name(), certItem.ID.Version(), nil) c.logger.Debug("sdk request 'keyvault.GetCertificate'", slog.String("request.certificateName", certItem.ID.Name()), slog.String("request.certificateVersion", certItem.ID.Version()), slog.Any("response", getCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'keyvault.GetCertificate': %w", err) } else { if !xcert.EqualCertificatesFromPEM(certPEM, string(getCertificateResp.CER)) { continue } } // 如果以上信息都一致,则视为已存在相同证书,直接返回 c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: string(*certItem.ID), CertName: certItem.ID.Name(), }, nil } } // 生成新证书名(需符合 Azure 命名规则) certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) // Azure Key Vault 不支持导入带有 Certificate Chain 的 PEM 证书。 // Issue Link: https://github.com/Azure/azure-cli/issues/19017 // 暂时的解决方法是,将 PEM 证书转换成 PFX 格式,然后再导入。 certPFX, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, "") if err != nil { return nil, fmt.Errorf("failed to transform certificate from PEM to PFX: %w", err) } // 导入证书 // REF: https://learn.microsoft.com/en-us/rest/api/keyvault/certificates/import-certificate/import-certificate importCertificateParams := azcertificates.ImportCertificateParameters{ Base64EncodedCertificate: to.Ptr(base64.StdEncoding.EncodeToString(certPFX)), CertificatePolicy: &azcertificates.CertificatePolicy{ SecretProperties: &azcertificates.SecretProperties{ ContentType: to.Ptr("application/x-pkcs12"), }, }, Tags: map[string]*string{ kvTagCertCN: to.Ptr(certCN), kvTagCertSN: to.Ptr(certSN), }, } importCertificateResp, err := c.sdkClient.ImportCertificate(ctx, certName, importCertificateParams, nil) c.logger.Debug("sdk request 'keyvault.ImportCertificate'", slog.String("request.certificateName", certName), slog.Any("request.parameters", importCertificateParams), slog.Any("response", importCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'keyvault.ImportCertificate': %w", err) } return &certmgr.UploadResult{ CertId: string(*importCertificateResp.ID), CertName: certName, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } // 转换证书格式 certPFX, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, "") if err != nil { return nil, fmt.Errorf("failed to transform certificate from PEM to PFX: %w", err) } // 获取证书 // REF: https://learn.microsoft.com/en-us/rest/api/keyvault/certificates/get-certificate/get-certificate getCertificateResp, err := c.sdkClient.GetCertificate(ctx, certIdOrName, "", nil) c.logger.Debug("sdk request 'keyvault.GetCertificate'", slog.String("request.certificateName", certIdOrName), slog.Any("response", getCertificateResp)) if err != nil { var respErr *azcore.ResponseError if !errors.As(err, &respErr) || (respErr.ErrorCode != "ResourceNotFound" && respErr.ErrorCode != "CertificateNotFound") { return nil, fmt.Errorf("failed to execute sdk request 'keyvault.GetCertificate': %w", err) } } else { // 如果已存在相同证书,直接返回 if xcert.EqualCertificatesFromPEM(certPEM, string(getCertificateResp.CER)) { return &certmgr.OperateResult{}, nil } } // 导入证书 // REF: https://learn.microsoft.com/en-us/rest/api/keyvault/certificates/import-certificate/import-certificate importCertificateParams := azcertificates.ImportCertificateParameters{ Base64EncodedCertificate: to.Ptr(base64.StdEncoding.EncodeToString(certPFX)), CertificatePolicy: &azcertificates.CertificatePolicy{ SecretProperties: &azcertificates.SecretProperties{ ContentType: to.Ptr("application/x-pkcs12"), }, }, Tags: map[string]*string{ kvTagCertCN: to.Ptr(certX509.Subject.CommonName), kvTagCertSN: to.Ptr(certX509.SerialNumber.Text(16)), }, } importCertificateResp, err := c.sdkClient.ImportCertificate(ctx, certIdOrName, importCertificateParams, nil) c.logger.Debug("sdk request 'keyvault.ImportCertificate'", slog.String("request.certificateName", certIdOrName), slog.Any("request.parameters", importCertificateParams), slog.Any("response", importCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'keyvault.ImportCertificate': %w", err) } return &certmgr.OperateResult{}, nil } const ( kvTagCertCN = "certimate/cert-cn" kvTagCertSN = "certimate/cert-sn" ) func createSDKClient(cloudName, tenantId, clientId, clientSecret, keyvaultName string) (*azcertificates.Client, error) { env, err := azenv.GetCloudEnvConfiguration(cloudName) if err != nil { return nil, err } clientOptions := azcore.ClientOptions{Cloud: env} credential, err := azidentity.NewClientSecretCredential(tenantId, clientId, clientSecret, &azidentity.ClientSecretCredentialOptions{ClientOptions: clientOptions}) if err != nil { return nil, err } endpoint := fmt.Sprintf("https://%s.vault.azure.net", keyvaultName) if azenv.IsUSGovernmentEnv(cloudName) { endpoint = fmt.Sprintf("https://%s.vault.usgovcloudapi.net", keyvaultName) } else if azenv.IsChinaEnv(cloudName) { endpoint = fmt.Sprintf("https://%s.vault.azure.cn", keyvaultName) } client, err := azcertificates.NewClient(endpoint, credential, nil) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/certmgr/providers/azure-keyvault/azure_keyvault_test.go ================================================ package azurekeyvault_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/azure-keyvault" ) var ( fInputCertPath string fInputKeyPath string fTenantId string fClientId string fClientSecret string fCloudName string fKeyVaultName string ) func init() { argsPrefix := "AZUREKEYVAULT_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fTenantId, argsPrefix+"TENANTID", "", "") flag.StringVar(&fClientId, argsPrefix+"CLIENTID", "", "") flag.StringVar(&fClientSecret, argsPrefix+"CLIENTSECRET", "", "") flag.StringVar(&fCloudName, argsPrefix+"CLOUDNAME", "", "") flag.StringVar(&fKeyVaultName, argsPrefix+"KEYVAULTNAME", "", "") } /* Shell command to run this test: go test -v ./azure_keyvault_test.go -args \ --AZUREKEYVAULT_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --AZUREKEYVAULT_INPUTKEYPATH="/path/to/your-input-key.pem" \ --AZUREKEYVAULT_TENANTID="your-tenant-id" \ --AZUREKEYVAULT_CLIENTID="your-app-registration-client-id" \ --AZUREKEYVAULT_CLIENTSECRET="your-app-registration-client-secret" \ --AZUREKEYVAULT_CLOUDNAME="china" \ --AZUREKEYVAULT_KEYVAULTNAME="your-keyvault-name" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("TENANTID: %v", fTenantId), fmt.Sprintf("CLIENTID: %v", fClientId), fmt.Sprintf("CLIENTSECRET: %v", fClientSecret), fmt.Sprintf("CLOUDNAME: %v", fCloudName), fmt.Sprintf("KEYVAULTNAME: %v", fKeyVaultName), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ TenantId: fTenantId, ClientId: fClientId, ClientSecret: fClientSecret, CloudName: fCloudName, KeyVaultName: fKeyVaultName, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/certmgr/providers/baiducloud-cert/baiducloud_cert.go ================================================ package baiducloudcert import ( "context" "errors" "fmt" "log/slog" "strings" "time" "github.com/certimate-go/certimate/pkg/core/certmgr" baiducert "github.com/certimate-go/certimate/pkg/sdk3rd/baiducloud/cert" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // 百度智能云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 百度智能云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *baiducert.Client } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } // 查看证书列表 // REF: https://cloud.baidu.com/doc/Reference/s/Gjwvz27xu#35-%E6%9F%A5%E7%9C%8B%E8%AF%81%E4%B9%A6%E5%88%97%E8%A1%A8%E8%AF%A6%E6%83%85 listCertDetail, err := c.sdkClient.ListCertDetail() c.logger.Debug("sdk request 'cert.ListCertDetail'", slog.Any("response", listCertDetail)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cert.ListCertDetail': %w", err) } else { for _, certItem := range listCertDetail.Certs { // 对比证书通用名称 if !strings.EqualFold(certX509.Subject.CommonName, certItem.CertCommonName) { continue } // 对比证书有效期 oldCertNotBefore, _ := time.Parse("2006-01-02T15:04:05Z", certItem.CertStartTime) oldCertNotAfter, _ := time.Parse("2006-01-02T15:04:05Z", certItem.CertStopTime) if !certX509.NotBefore.Equal(oldCertNotBefore) || !certX509.NotAfter.Equal(oldCertNotAfter) { continue } // 对比证书多域名 if certItem.CertDNSNames != strings.Join(certX509.DNSNames, ",") { continue } // 对比证书内容 getCertDetailResp, err := c.sdkClient.GetCertRawData(certItem.CertId) c.logger.Debug("sdk request 'cert.GetCertRawData'", slog.Any("certId", certItem.CertId), slog.Any("response", getCertDetailResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cert.GetCertRawData': %w", err) } else { if !xcert.EqualCertificatesFromPEM(certPEM, getCertDetailResp.CertServerData) { continue } } // 如果以上信息都一致,则视为已存在相同证书,直接返回 c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: certItem.CertId, CertName: certItem.CertName, }, nil } } // 创建证书 // REF: https://cloud.baidu.com/doc/Reference/s/Gjwvz27xu#31-%E5%88%9B%E5%BB%BA%E8%AF%81%E4%B9%A6 createCertReq := &baiducert.CreateCertArgs{} createCertReq.CertName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) createCertReq.CertServerData = certPEM createCertReq.CertPrivateData = privkeyPEM createCertResp, err := c.sdkClient.CreateCert(createCertReq) c.logger.Debug("sdk request 'cert.CreateCert'", slog.Any("request", createCertReq), slog.Any("response", createCertResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cert.CreateCert': %w", err) } return &certmgr.UploadResult{ CertId: createCertResp.CertId, CertName: createCertResp.CertName, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func createSDKClient(accessKeyId, secretAccessKey string) (*baiducert.Client, error) { client, err := baiducert.NewClient(accessKeyId, secretAccessKey, "") if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/certmgr/providers/baiducloud-cert/baiducloud_cert_test.go ================================================ package baiducloudcert_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/baiducloud-cert" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string ) func init() { argsPrefix := "BAIDUCLOUDCERT_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") } /* Shell command to run this test: go test -v ./baiducloud_cert_test.go -args \ --BAIDUCLOUDCERT_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --BAIDUCLOUDCERT_INPUTKEYPATH="/path/to/your-input-key.pem" \ --BAIDUCLOUDCERT_ACCESSKEYID="your-access-key-id" \ --BAIDUCLOUDCERT_SECRETACCESSKEY="your-access-key-secret" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/certmgr/providers/baishan-cdn/baishan_cdn.go ================================================ package baishancdn import ( "context" "errors" "fmt" "log/slog" "regexp" "strings" "time" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" baishansdk "github.com/certimate-go/certimate/pkg/sdk3rd/baishan" ) type CertmgrConfig struct { // 白山云 API Token。 ApiToken string `json:"apiToken"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *baishansdk.Client } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.ApiToken) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (d *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } } func (d *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 生成新证书名(需符合白山云命名规则) certName := fmt.Sprintf("certimate_%d", time.Now().UnixMilli()) // 新增证书 // REF: https://portal.baishancloud.com/track/document/downloadPdf/1441 certId := "" uploadDomainCertificateReq := &baishansdk.UploadDomainCertificateRequest{ Name: lo.ToPtr(certName), Certificate: lo.ToPtr(certPEM), Key: lo.ToPtr(privkeyPEM), } uploadDomainCertificateResp, err := d.sdkClient.UploadDomainCertificateWithContext(ctx, uploadDomainCertificateReq) d.logger.Debug("sdk request 'baishan.UploadDomainCertificate'", slog.Any("request", uploadDomainCertificateReq), slog.Any("response", uploadDomainCertificateResp)) if err != nil { if uploadDomainCertificateResp != nil { if uploadDomainCertificateResp.GetCode() == 400699 && strings.Contains(uploadDomainCertificateResp.GetMessage(), "this certificate is exists") { // 证书已存在,忽略新增证书接口错误 re := regexp.MustCompile(`\d+`) certId = re.FindString(uploadDomainCertificateResp.GetMessage()) } } if certId == "" { return nil, fmt.Errorf("failed to execute sdk request 'baishan.SetDomainCertificate': %w", err) } } else { certId = uploadDomainCertificateResp.Data.CertId.String() } return &certmgr.UploadResult{ CertId: certId, CertName: certName, }, nil } func (d *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { // 替换证书 // REF: https://portal.baishancloud.com/track/document/downloadPdf/1441 uploadDomainCertificateReq := &baishansdk.UploadDomainCertificateRequest{ CertificateId: lo.ToPtr(certIdOrName), Name: lo.ToPtr(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())), Certificate: lo.ToPtr(certPEM), Key: lo.ToPtr(privkeyPEM), } uploadDomainCertificateResp, err := d.sdkClient.UploadDomainCertificateWithContext(ctx, uploadDomainCertificateReq) d.logger.Debug("sdk request 'baishan.UploadDomainCertificate'", slog.Any("request", uploadDomainCertificateReq), slog.Any("response", uploadDomainCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'baishan.UploadDomainCertificate': %w", err) } return &certmgr.OperateResult{}, nil } func createSDKClient(apiToken string) (*baishansdk.Client, error) { return baishansdk.NewClient(apiToken) } ================================================ FILE: pkg/core/certmgr/providers/baishan-cdn/baishan_cdn_test.go ================================================ package baishancdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/baishan-cdn" ) var ( fInputCertPath string fInputKeyPath string fApiToken string ) func init() { argsPrefix := "BAISHANCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "") } /* Shell command to run this test: go test -v ./baishan_cdn_test.go -args \ --BAISHANCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --BAISHANCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --BAISHANCDN_APITOKEN="your-api-token" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("APITOKEN: %v", fApiToken), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ ApiToken: fApiToken, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/certmgr/providers/byteplus-cdn/byteplus_cdn.go ================================================ package bytepluscdn import ( "context" "crypto/sha1" "crypto/sha256" "encoding/hex" "errors" "fmt" "log/slog" "strings" "time" bytepluscdn "github.com/byteplus-sdk/byteplus-sdk-golang/service/cdn" "github.com/certimate-go/certimate/pkg/core/certmgr" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // BytePlus AccessKey。 AccessKey string `json:"accessKey"` // BytePlus SecretKey。 SecretKey string `json:"secretKey"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *bytepluscdn.CDN } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client := bytepluscdn.NewInstance() client.Client.SetAccessKey(config.AccessKey) client.Client.SetSecretKey(config.SecretKey) return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } // 查询证书列表,避免重复上传 // REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-listcertinfo listCertInfoPageNum := 1 listCertInfoPageSize := 100 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } listCertInfoReq := &bytepluscdn.ListCertInfoRequest{ PageNum: bytepluscdn.GetInt64Ptr(int64(listCertInfoPageNum)), PageSize: bytepluscdn.GetInt64Ptr(int64(listCertInfoPageSize)), Source: bytepluscdn.GetStrPtr("cert_center"), } listCertInfoResp, err := c.sdkClient.ListCertInfo(listCertInfoReq) c.logger.Debug("sdk request 'cdn.ListCertInfo'", slog.Any("request", listCertInfoReq), slog.Any("response", listCertInfoResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.ListCertInfo': %w", err) } for _, certItem := range listCertInfoResp.Result.CertInfo { // 对比证书 SHA-1 摘要 fingerprintSha1 := sha1.Sum(certX509.Raw) if !strings.EqualFold(hex.EncodeToString(fingerprintSha1[:]), certItem.CertFingerprint.Sha1) { continue } // 对比证书 SHA-256 摘要 fingerprintSha256 := sha256.Sum256(certX509.Raw) if !strings.EqualFold(hex.EncodeToString(fingerprintSha256[:]), certItem.CertFingerprint.Sha256) { continue } // 如果以上信息都一致,则视为已存在相同证书,直接返回 c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: certItem.CertId, CertName: certItem.Desc, }, nil } if len(listCertInfoResp.Result.CertInfo) < listCertInfoPageSize { break } listCertInfoPageNum++ } // 生成新证书名(需符合 BytePlus 命名规则) certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) // 上传新证书 // REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-addcertificate addCertificateReq := &bytepluscdn.AddCertificateRequest{ Certificate: certPEM, PrivateKey: privkeyPEM, Source: bytepluscdn.GetStrPtr("cert_center"), Desc: bytepluscdn.GetStrPtr(certName), } addCertificateResp, err := c.sdkClient.AddCertificate(addCertificateReq) c.logger.Debug("sdk request 'cdn.AddCertificate'", slog.Any("request", addCertificateReq), slog.Any("response", addCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.AddCertificate': %w", err) } return &certmgr.UploadResult{ CertId: addCertificateResp.Result.CertId, CertName: certName, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } ================================================ FILE: pkg/core/certmgr/providers/ctcccloud-ao/ctcccloud_ao.go ================================================ package ctcccloudao import ( "context" "errors" "fmt" "log/slog" "slices" "strings" "time" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" ctyunao "github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/ao" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // 天翼云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 天翼云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *ctyunao.Client } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } // 查询用户名下证书列表,避免重复上传 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13175&data=174&isNormal=1&vid=167 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13015&data=174&isNormal=1&vid=167 listCertPage := 1 listCertPerPage := 1000 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } listCertsReq := &ctyunao.ListCertsRequest{ Page: lo.ToPtr(int32(listCertPage)), PerPage: lo.ToPtr(int32(listCertPerPage)), UsageMode: lo.ToPtr(int32(0)), } listCertsResp, err := c.sdkClient.ListCertsWithContext(ctx, listCertsReq) c.logger.Debug("sdk request 'ao.ListCerts'", slog.Any("request", listCertsReq), slog.Any("response", listCertsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'ao.ListCerts': %w", err) } if listCertsResp.ReturnObj == nil { break } for _, certItem := range listCertsResp.ReturnObj.Results { // 对比证书通用名称 if !strings.EqualFold(certX509.Subject.CommonName, certItem.CN) { continue } // 对比证书扩展名称 if !slices.Equal(certX509.DNSNames, certItem.SANs) { continue } // 对比证书有效期 if !certX509.NotBefore.Equal(time.Unix(certItem.IssueTime, 0).UTC()) { continue } else if !certX509.NotAfter.Equal(time.Unix(certItem.ExpiresTime, 0).UTC()) { continue } // 对比证书内容 queryCertReq := &ctyunao.QueryCertRequest{ Id: lo.ToPtr(certItem.Id), } queryCertResp, err := c.sdkClient.QueryCertWithContext(ctx, queryCertReq) c.logger.Debug("sdk request 'ao.QueryCert'", slog.Any("request", queryCertReq), slog.Any("response", queryCertResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'ao.QueryCert': %w", err) } else if queryCertResp.ReturnObj != nil && queryCertResp.ReturnObj.Result != nil { if !xcert.EqualCertificatesFromPEM(certPEM, queryCertResp.ReturnObj.Result.Certs) { continue } } // 如果以上信息都一致,则视为已存在相同证书,直接返回 c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: fmt.Sprintf("%d", queryCertResp.ReturnObj.Result.Id), CertName: queryCertResp.ReturnObj.Result.Name, }, nil } if len(listCertsResp.ReturnObj.Results) < listCertPerPage { break } listCertPage++ } // 生成新证书名(需符合天翼云命名规则) certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) // 创建证书 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13014&data=174&isNormal=1&vid=167 createCertReq := &ctyunao.CreateCertRequest{ Name: lo.ToPtr(certName), Certs: lo.ToPtr(certPEM), Key: lo.ToPtr(privkeyPEM), } createCertResp, err := c.sdkClient.CreateCertWithContext(ctx, createCertReq) c.logger.Debug("sdk request 'ao.CreateCert'", slog.Any("request", createCertReq), slog.Any("response", createCertResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'ao.CreateCert': %w", err) } return &certmgr.UploadResult{ CertId: fmt.Sprintf("%d", createCertResp.ReturnObj.Id), CertName: certName, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func createSDKClient(accessKeyId, secretAccessKey string) (*ctyunao.Client, error) { return ctyunao.NewClient(accessKeyId, secretAccessKey) } ================================================ FILE: pkg/core/certmgr/providers/ctcccloud-ao/ctcccloud_ao_test.go ================================================ package ctcccloudao_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-ao" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string ) func init() { argsPrefix := "CTCCCLOUDAO_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") } /* Shell command to run this test: go test -v ./ctcccloud_ao_test.go -args \ --CTCCCLOUDAO_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --CTCCCLOUDAO_INPUTKEYPATH="/path/to/your-input-key.pem" \ --CTCCCLOUDAO_ACCESSKEYID="your-access-key-id" \ --CTCCCLOUDAO_SECRETACCESSKEY="your-secret-access-key" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/certmgr/providers/ctcccloud-cdn/ctcccloud_cdn.go ================================================ package ctcccloudcdn import ( "context" "errors" "fmt" "log/slog" "slices" "strings" "time" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" ctyuncdn "github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/cdn" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // 天翼云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 天翼云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *ctyuncdn.Client } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } // 查询证书列表,避免重复上传 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=10901&data=161&isNormal=1&vid=154 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=10899&data=161&isNormal=1&vid=154 queryCertListPage := 1 queryCertListPerPage := 1000 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } queryCertListReq := &ctyuncdn.QueryCertListRequest{ Page: lo.ToPtr(int32(queryCertListPage)), PerPage: lo.ToPtr(int32(queryCertListPerPage)), UsageMode: lo.ToPtr(int32(0)), } queryCertListResp, err := c.sdkClient.QueryCertListWithContext(ctx, queryCertListReq) c.logger.Debug("sdk request 'cdn.QueryCertList'", slog.Any("request", queryCertListReq), slog.Any("response", queryCertListResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.QueryCertList': %w", err) } if queryCertListResp.ReturnObj == nil { break } for _, certItem := range queryCertListResp.ReturnObj.Results { // 对比证书通用名称 if !strings.EqualFold(certX509.Subject.CommonName, certItem.CN) { continue } // 对比证书扩展名称 if !slices.Equal(certX509.DNSNames, certItem.SANs) { continue } // 对比证书有效期 if !certX509.NotBefore.Equal(time.Unix(certItem.IssueTime, 0).UTC()) { continue } else if !certX509.NotAfter.Equal(time.Unix(certItem.ExpiresTime, 0).UTC()) { continue } // 对比证书内容 queryCertDetailReq := &ctyuncdn.QueryCertDetailRequest{ Id: lo.ToPtr(certItem.Id), } queryCertDetailResp, err := c.sdkClient.QueryCertDetailWithContext(ctx, queryCertDetailReq) c.logger.Debug("sdk request 'cdn.QueryCertDetail'", slog.Any("request", queryCertDetailReq), slog.Any("response", queryCertDetailResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.QueryCertDetail': %w", err) } else if queryCertDetailResp.ReturnObj != nil && queryCertDetailResp.ReturnObj.Result != nil { if !xcert.EqualCertificatesFromPEM(certPEM, queryCertDetailResp.ReturnObj.Result.Certs) { continue } } // 如果以上信息都一致,则视为已存在相同证书,直接返回 c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: fmt.Sprintf("%d", queryCertDetailResp.ReturnObj.Result.Id), CertName: queryCertDetailResp.ReturnObj.Result.Name, }, nil } if len(queryCertListResp.ReturnObj.Results) < queryCertListPerPage { break } queryCertListPage++ } // 生成新证书名(需符合天翼云命名规则) certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) // 创建证书 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=10893&data=161&isNormal=1&vid=154 createCertReq := &ctyuncdn.CreateCertRequest{ Name: lo.ToPtr(certName), Certs: lo.ToPtr(certPEM), Key: lo.ToPtr(privkeyPEM), } createCertResp, err := c.sdkClient.CreateCertWithContext(ctx, createCertReq) c.logger.Debug("sdk request 'cdn.CreateCert'", slog.Any("request", createCertReq), slog.Any("response", createCertResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.CreateCert': %w", err) } return &certmgr.UploadResult{ CertId: fmt.Sprintf("%d", createCertResp.ReturnObj.Id), CertName: certName, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func createSDKClient(accessKeyId, secretAccessKey string) (*ctyuncdn.Client, error) { return ctyuncdn.NewClient(accessKeyId, secretAccessKey) } ================================================ FILE: pkg/core/certmgr/providers/ctcccloud-cdn/ctcccloud_cdn_test.go ================================================ package ctcccloudcdn_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-cdn" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string ) func init() { argsPrefix := "CTCCCLOUDCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") } /* Shell command to run this test: go test -v ./ctcccloud_cdn_test.go -args \ --CTCCCLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --CTCCCLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --CTCCCLOUDCDN_ACCESSKEYID="your-access-key-id" \ --CTCCCLOUDCDN_SECRETACCESSKEY="your-secret-access-key" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/certmgr/providers/ctcccloud-cms/ctcccloud_cms.go ================================================ package ctcccloudcms import ( "context" "crypto/sha1" "encoding/hex" "errors" "fmt" "log/slog" "strings" "time" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" ctyuncms "github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/cms" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // 天翼云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 天翼云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *ctyuncms.Client } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 避免重复上传 if upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM); err != nil { return nil, err } else if upok { c.logger.Info("ssl certificate already exists") return upres, nil } // 提取服务器证书和中间证书 serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM) if err != nil { return nil, fmt.Errorf("failed to extract certs: %w", err) } // 生成新证书名(需符合天翼云命名规则) certName := fmt.Sprintf("cm%d", time.Now().Unix()) // 上传证书 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=152&api=17243&data=204&isNormal=1&vid=283 uploadCertificateReq := &ctyuncms.UploadCertificateRequest{ Name: lo.ToPtr(certName), Certificate: lo.ToPtr(serverCertPEM), CertificateChain: lo.ToPtr(intermediaCertPEM), PrivateKey: lo.ToPtr(privkeyPEM), EncryptionStandard: lo.ToPtr("INTERNATIONAL"), } uploadCertificateResp, err := c.sdkClient.UploadCertificateWithContext(ctx, uploadCertificateReq) c.logger.Debug("sdk request 'cms.UploadCertificate'", slog.Any("request", uploadCertificateReq), slog.Any("response", uploadCertificateResp)) if err != nil { if uploadCertificateResp != nil && uploadCertificateResp.GetError() == "CCMS_100000067" { if upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM); err != nil { return nil, err } else if !upok { return nil, errors.New("ctyun cms: no certificate found") } else { c.logger.Info("ssl certificate already exists") return upres, nil } } return nil, fmt.Errorf("failed to execute sdk request 'cms.UploadCertificate': %w", err) } // 获取刚刚上传证书 ID if upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM); err != nil { return nil, err } else if !upok { return nil, fmt.Errorf("could not find ssl certificate, may be upload failed") } else { return upres, nil } } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func (c *Certmgr) tryGetResultIfCertExists(ctx context.Context, certPEM string) (*certmgr.UploadResult, bool, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, false, err } // 查询用户证书列表 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=152&api=17233&data=204&isNormal=1&vid=283 getCertificateListPageNum := 1 getCertificateListPageSize := 10 for { select { case <-ctx.Done(): return nil, false, ctx.Err() default: } getCertificateListReq := &ctyuncms.GetCertificateListRequest{ PageNum: lo.ToPtr(int32(getCertificateListPageNum)), PageSize: lo.ToPtr(int32(getCertificateListPageSize)), Keyword: lo.ToPtr(certX509.Subject.CommonName), Origin: lo.ToPtr("UPLOAD"), } getCertificateListResp, err := c.sdkClient.GetCertificateListWithContext(ctx, getCertificateListReq) c.logger.Debug("sdk request 'cms.GetCertificateList'", slog.Any("request", getCertificateListReq), slog.Any("response", getCertificateListResp)) if err != nil { return nil, false, fmt.Errorf("failed to execute sdk request 'cms.GetCertificateList': %w", err) } if getCertificateListResp.ReturnObj == nil { break } for _, certItem := range getCertificateListResp.ReturnObj.List { // 对比证书名称 if !strings.EqualFold(strings.Join(certX509.DNSNames, ","), certItem.DomainName) { continue } // 对比证书有效期 oldCertNotBefore, _ := time.Parse("2006-01-02T15:04:05Z", certItem.IssueTime) oldCertNotAfter, _ := time.Parse("2006-01-02T15:04:05Z", certItem.ExpireTime) if !certX509.NotBefore.Equal(oldCertNotBefore) { continue } else if !certX509.NotAfter.Equal(oldCertNotAfter) { continue } // 对比证书指纹 fingerprint := sha1.Sum(certX509.Raw) fingerprintHex := hex.EncodeToString(fingerprint[:]) if !strings.EqualFold(fingerprintHex, certItem.Fingerprint) { continue } // 如果以上信息都一致,则视为已存在相同证书,直接返回 c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: certItem.Id, CertName: certItem.Name, }, true, nil } if len(getCertificateListResp.ReturnObj.List) < getCertificateListPageSize { break } getCertificateListPageNum++ } return nil, false, nil } func createSDKClient(accessKeyId, secretAccessKey string) (*ctyuncms.Client, error) { return ctyuncms.NewClient(accessKeyId, secretAccessKey) } ================================================ FILE: pkg/core/certmgr/providers/ctcccloud-cms/ctcccloud_cms_test.go ================================================ package ctcccloudcms_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-cms" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string ) func init() { argsPrefix := "CTCCCLOUDCMS_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") } /* Shell command to run this test: go test -v ./ctcccloud_cms_test.go -args \ --CTCCCLOUDCMS_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --CTCCCLOUDCMS_INPUTKEYPATH="/path/to/your-input-key.pem" \ --CTCCCLOUDCMS_ACCESSKEYID="your-access-key-id" \ --CTCCCLOUDCMS_SECRETACCESSKEY="your-secret-access-key" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/certmgr/providers/ctcccloud-elb/ctcccloud_elb.go ================================================ package ctcccloudelb import ( "context" "errors" "fmt" "log/slog" "time" "github.com/pocketbase/pocketbase/tools/security" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" ctyunelb "github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/elb" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // 天翼云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 天翼云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // 天翼云资源池 ID。 RegionId string `json:"regionId"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *ctyunelb.Client } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 查询证书列表,避免重复上传 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=24&api=5692&data=88&isNormal=1&vid=82 listCertificatesReq := &ctyunelb.ListCertificatesRequest{ RegionID: lo.ToPtr(c.config.RegionId), } listCertificatesResp, err := c.sdkClient.ListCertificatesWithContext(ctx, listCertificatesReq) c.logger.Debug("sdk request 'elb.ListCertificates'", slog.Any("request", listCertificatesReq), slog.Any("response", listCertificatesResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'elb.ListCertificates': %w", err) } else { for _, certItem := range listCertificatesResp.ReturnObj { // 如果已存在相同证书,直接返回 if xcert.EqualCertificatesFromPEM(certPEM, certItem.Certificate) { c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: certItem.ID, CertName: certItem.Name, }, nil } } } // 生成新证书名(需符合天翼云命名规则) certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) // 创建证书 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=24&api=5685&data=88&isNormal=1&vid=82 createCertificateReq := &ctyunelb.CreateCertificateRequest{ ClientToken: lo.ToPtr(security.RandomString(32)), RegionID: lo.ToPtr(c.config.RegionId), Name: lo.ToPtr(certName), Description: lo.ToPtr("upload from certimate"), Type: lo.ToPtr("Server"), Certificate: lo.ToPtr(certPEM), PrivateKey: lo.ToPtr(privkeyPEM), } createCertificateResp, err := c.sdkClient.CreateCertificateWithContext(ctx, createCertificateReq) c.logger.Debug("sdk request 'elb.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'elb.CreateCertificate': %w", err) } return &certmgr.UploadResult{ CertId: createCertificateResp.ReturnObj.ID, CertName: certName, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func createSDKClient(accessKeyId, secretAccessKey string) (*ctyunelb.Client, error) { return ctyunelb.NewClient(accessKeyId, secretAccessKey) } ================================================ FILE: pkg/core/certmgr/providers/ctcccloud-elb/ctcccloud_elb_test.go ================================================ package ctcccloudelb_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-elb" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string fRegionId string ) func init() { argsPrefix := "CTCCCLOUDELB_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") flag.StringVar(&fRegionId, argsPrefix+"REGIONID", "", "") } /* Shell command to run this test: go test -v ./ctcccloud_elb_test.go -args \ --CTCCCLOUDELB_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --CTCCCLOUDELB_INPUTKEYPATH="/path/to/your-input-key.pem" \ --CTCCCLOUDELB_ACCESSKEYID="your-access-key-id" \ --CTCCCLOUDELB_SECRETACCESSKEY="your-secret-access-key" \ --CTCCCLOUDELB_REGIONID="your-region-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), fmt.Sprintf("REGIONID: %v", fRegionId), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, RegionId: fRegionId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/certmgr/providers/ctcccloud-icdn/ctcccloud_icdn.go ================================================ package ctcccloudicdn import ( "context" "errors" "fmt" "log/slog" "slices" "strings" "time" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" ctyunicdn "github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/icdn" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // 天翼云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 天翼云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *ctyunicdn.Client } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } // 查询证书列表,避免重复上传 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10838&data=173&isNormal=1&vid=166 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10837&data=173&isNormal=1&vid=166 queryCertListPage := 1 queryCertListPerPage := 1000 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } queryCertListReq := &ctyunicdn.QueryCertListRequest{ Page: lo.ToPtr(int32(queryCertListPage)), PerPage: lo.ToPtr(int32(queryCertListPerPage)), UsageMode: lo.ToPtr(int32(0)), } queryCertListResp, err := c.sdkClient.QueryCertListWithContext(ctx, queryCertListReq) c.logger.Debug("sdk request 'icdn.QueryCertList'", slog.Any("request", queryCertListReq), slog.Any("response", queryCertListResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'icdn.QueryCertList': %w", err) } if queryCertListResp.ReturnObj == nil { break } for _, certItem := range queryCertListResp.ReturnObj.Results { // 对比证书通用名称 if !strings.EqualFold(certX509.Subject.CommonName, certItem.CN) { continue } // 对比证书扩展名称 if !slices.Equal(certX509.DNSNames, certItem.SANs) { continue } // 对比证书有效期 if !certX509.NotBefore.Equal(time.Unix(certItem.IssueTime, 0).UTC()) { continue } else if !certX509.NotAfter.Equal(time.Unix(certItem.ExpiresTime, 0).UTC()) { continue } // 对比证书内容 queryCertDetailReq := &ctyunicdn.QueryCertDetailRequest{ Id: lo.ToPtr(certItem.Id), } queryCertDetailResp, err := c.sdkClient.QueryCertDetailWithContext(ctx, queryCertDetailReq) c.logger.Debug("sdk request 'icdn.QueryCertDetail'", slog.Any("request", queryCertDetailReq), slog.Any("response", queryCertDetailResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'icdn.QueryCertDetail': %w", err) } else if queryCertDetailResp.ReturnObj != nil && queryCertDetailResp.ReturnObj.Result != nil { if !xcert.EqualCertificatesFromPEM(certPEM, queryCertDetailResp.ReturnObj.Result.Certs) { continue } } // 如果以上信息都一致,则视为已存在相同证书,直接返回 c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: fmt.Sprintf("%d", queryCertDetailResp.ReturnObj.Result.Id), CertName: queryCertDetailResp.ReturnObj.Result.Name, }, nil } if len(queryCertListResp.ReturnObj.Results) < queryCertListPerPage { break } queryCertListPage++ } // 生成新证书名(需符合天翼云命名规则) certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) // 创建证书 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10835&data=173&isNormal=1&vid=166 createCertReq := &ctyunicdn.CreateCertRequest{ Name: lo.ToPtr(certName), Certs: lo.ToPtr(certPEM), Key: lo.ToPtr(privkeyPEM), } createCertResp, err := c.sdkClient.CreateCertWithContext(ctx, createCertReq) c.logger.Debug("sdk request 'icdn.CreateCert'", slog.Any("request", createCertReq), slog.Any("response", createCertResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'icdn.CreateCert': %w", err) } return &certmgr.UploadResult{ CertId: fmt.Sprintf("%d", createCertResp.ReturnObj.Id), CertName: certName, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func createSDKClient(accessKeyId, secretAccessKey string) (*ctyunicdn.Client, error) { return ctyunicdn.NewClient(accessKeyId, secretAccessKey) } ================================================ FILE: pkg/core/certmgr/providers/ctcccloud-icdn/ctcccloud_icdn_test.go ================================================ package ctcccloudicdn_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-icdn" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string ) func init() { argsPrefix := "CTCCCLOUDICDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") } /* Shell command to run this test: go test -v ./ctcccloud_icdn_test.go -args \ --CTCCCLOUDICDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --CTCCCLOUDICDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --CTCCCLOUDICDN_ACCESSKEYID="your-access-key-id" \ --CTCCCLOUDICDN_SECRETACCESSKEY="your-secret-access-key" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/certmgr/providers/ctcccloud-lvdn/ctcccloud_lvdn.go ================================================ package ctcccloudlvdn import ( "context" "errors" "fmt" "log/slog" "slices" "strings" "time" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" ctyunlvdn "github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/lvdn" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // 天翼云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 天翼云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *ctyunlvdn.Client } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } // 查询证书列表,避免重复上传 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11452&data=183&isNormal=1&vid=261 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11449&data=183&isNormal=1&vid=261 queryCertListPage := 1 queryCertListPerPage := 1000 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } queryCertListReq := &ctyunlvdn.QueryCertListRequest{ Page: lo.ToPtr(int32(queryCertListPage)), PerPage: lo.ToPtr(int32(queryCertListPerPage)), UsageMode: lo.ToPtr(int32(0)), } queryCertListResp, err := c.sdkClient.QueryCertListWithContext(ctx, queryCertListReq) c.logger.Debug("sdk request 'lvdn.QueryCertList'", slog.Any("request", queryCertListReq), slog.Any("response", queryCertListResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'lvdn.QueryCertList': %w", err) } if queryCertListResp.ReturnObj == nil { break } for _, certItem := range queryCertListResp.ReturnObj.Results { // 对比证书通用名称 if !strings.EqualFold(certX509.Subject.CommonName, certItem.CN) { continue } // 对比证书扩展名称 if !slices.Equal(certX509.DNSNames, certItem.SANs) { continue } // 对比证书有效期 if !certX509.NotBefore.Equal(time.Unix(certItem.IssueTime, 0).UTC()) { continue } else if !certX509.NotAfter.Equal(time.Unix(certItem.ExpiresTime, 0).UTC()) { continue } // 对比证书内容 queryCertDetailReq := &ctyunlvdn.QueryCertDetailRequest{ Id: lo.ToPtr(certItem.Id), } queryCertDetailResp, err := c.sdkClient.QueryCertDetailWithContext(ctx, queryCertDetailReq) c.logger.Debug("sdk request 'lvdn.QueryCertDetail'", slog.Any("request", queryCertDetailReq), slog.Any("response", queryCertDetailResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'lvdn.QueryCertDetail': %w", err) } else if queryCertDetailResp.ReturnObj != nil && queryCertDetailResp.ReturnObj.Result != nil { if !xcert.EqualCertificatesFromPEM(certPEM, queryCertDetailResp.ReturnObj.Result.Certs) { continue } } // 如果以上信息都一致,则视为已存在相同证书,直接返回 c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: fmt.Sprintf("%d", queryCertDetailResp.ReturnObj.Result.Id), CertName: queryCertDetailResp.ReturnObj.Result.Name, }, nil } if len(queryCertListResp.ReturnObj.Results) < queryCertListPerPage { break } queryCertListPage++ } // 生成新证书名(需符合天翼云命名规则) certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) // 创建证书 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11436&data=183&isNormal=1&vid=261 createCertReq := &ctyunlvdn.CreateCertRequest{ Name: lo.ToPtr(certName), Certs: lo.ToPtr(certPEM), Key: lo.ToPtr(privkeyPEM), } createCertResp, err := c.sdkClient.CreateCertWithContext(ctx, createCertReq) c.logger.Debug("sdk request 'lvdn.CreateCert'", slog.Any("request", createCertReq), slog.Any("response", createCertResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'lvdn.CreateCert': %w", err) } return &certmgr.UploadResult{ CertId: fmt.Sprintf("%d", createCertResp.ReturnObj.Id), CertName: certName, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func createSDKClient(accessKeyId, secretAccessKey string) (*ctyunlvdn.Client, error) { return ctyunlvdn.NewClient(accessKeyId, secretAccessKey) } ================================================ FILE: pkg/core/certmgr/providers/ctcccloud-lvdn/ctcccloud_lvdn_test.go ================================================ package ctcccloudlvdn_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-lvdn" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string ) func init() { argsPrefix := "CTCCCLOUDLVDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") } /* Shell command to run this test: go test -v ./ctcccloud_lvdn_test.go -args \ --CTCCCLOUDLVDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --CTCCCLOUDLVDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --CTCCCLOUDLVDN_ACCESSKEYID="your-access-key-id" \ --CTCCCLOUDLVDN_SECRETACCESSKEY="your-secret-access-key" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/certmgr/providers/dogecloud/dogecloud.go ================================================ package dogecloud import ( "context" "errors" "fmt" "log/slog" "time" "github.com/certimate-go/certimate/pkg/core/certmgr" dogesdk "github.com/certimate-go/certimate/pkg/sdk3rd/dogecloud" ) type CertmgrConfig struct { // 多吉云 AccessKey。 AccessKey string `json:"accessKey"` // 多吉云 SecretKey。 SecretKey string `json:"secretKey"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *dogesdk.Client } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.AccessKey, config.SecretKey) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 生成新证书名(需符合多吉云命名规则) certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) // 上传新证书 // REF: https://docs.dogecloud.com/cdn/api-cert-upload uploadSslCertReq := &dogesdk.UploadCdnCertRequest{ Note: certName, Certificate: certPEM, PrivateKey: privkeyPEM, } uploadSslCertResp, err := c.sdkClient.UploadCdnCertWithContext(ctx, uploadSslCertReq) c.logger.Debug("sdk request 'cdn.UploadCdnCert'", slog.Any("request", uploadSslCertReq), slog.Any("response", uploadSslCertResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.UploadCdnCert': %w", err) } return &certmgr.UploadResult{ CertId: fmt.Sprintf("%d", uploadSslCertResp.Data.Id), CertName: certName, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func createSDKClient(accessKey, secretKey string) (*dogesdk.Client, error) { return dogesdk.NewClient(accessKey, secretKey) } ================================================ FILE: pkg/core/certmgr/providers/dokploy/dokploy.go ================================================ package dokploy import ( "context" "crypto/tls" "errors" "fmt" "log/slog" "time" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" dokploysdk "github.com/certimate-go/certimate/pkg/sdk3rd/dokploy" ) type CertmgrConfig struct { // Dokploy 服务地址。 ServerUrl string `json:"serverUrl"` // Dokploy API Key。 ApiKey string `json:"apiKey"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *dokploysdk.Client } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 查询证书列表,避免重复上传 // REF: https://docs.dokploy.com/docs/api/certificates#certificates-all certificatesAllReq := &dokploysdk.CertificatesAllRequest{} certificatesAllResp, err := c.sdkClient.CertificatesAllWithContext(ctx, certificatesAllReq) c.logger.Debug("sdk request 'certificates.all'", slog.Any("request", certificatesAllReq), slog.Any("response", certificatesAllResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'certificates.all': %w", err) } else { for _, certItem := range *certificatesAllResp { if certItem.CertificateData == certPEM && certItem.PrivateKey == privkeyPEM { // 如果已存在相同证书,直接返回 c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: certItem.CertificateId, CertName: certItem.Name, }, nil } } } // 获取账号信息,找到默认的组织 ID // REF: https://docs.dokploy.com/docs/api/reference-user#user.get userGetReq := &dokploysdk.UserGetRequest{} userGetResp, err := c.sdkClient.UserGetWithContext(ctx, userGetReq) c.logger.Debug("sdk request 'user.get'", slog.Any("request", userGetReq), slog.Any("response", userGetResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'user.get': %w", err) } // 创建证书 // REF: https://docs.dokploy.com/docs/api/certificates#certificates-create certificatesCreateReq := &dokploysdk.CertificatesCreateRequest{ Name: lo.ToPtr(fmt.Sprintf("certimate-%d", time.Now().Unix())), CertificateData: lo.ToPtr(certPEM), PrivateKey: lo.ToPtr(privkeyPEM), OrganizationId: lo.ToPtr(userGetResp.OrganizationId), } certificatesCreateResp, err := c.sdkClient.CertificatesCreateWithContext(ctx, certificatesCreateReq) c.logger.Debug("sdk request 'certificates.create'", slog.Any("request", certificatesCreateReq), slog.Any("response", certificatesCreateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'certificates.create': %w", err) } return &certmgr.UploadResult{ CertId: certificatesCreateResp.CertificateId, CertName: certificatesCreateResp.Name, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*dokploysdk.Client, error) { client, err := dokploysdk.NewClient(serverUrl, apiKey) if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } ================================================ FILE: pkg/core/certmgr/providers/dokploy/dokploy_test.go ================================================ package dokploy_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/dokploy" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fApiKey string ) func init() { argsPrefix := "DOKPLOY_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") } /* Shell command to run this test: go test -v ./dokploy_test.go -args \ --DOKPLOY_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --DOKPLOY_INPUTKEYPATH="/path/to/your-input-key.pem" \ --DOKPLOY_SERVERURL="http://127.0.0.1:3000" \ --DOKPLOY_APIKEY="your-api-key" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("APIKEY: %v", fApiKey), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ ServerUrl: fServerUrl, ApiKey: fApiKey, AllowInsecureConnections: true, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/certmgr/providers/gcore-cdn/gcore_cdn.go ================================================ package gcorecdn import ( "context" "errors" "fmt" "log/slog" "time" gcore "github.com/G-Core/gcorelabscdn-go/gcore/provider" "github.com/G-Core/gcorelabscdn-go/sslcerts" "github.com/certimate-go/certimate/pkg/core/certmgr" gcoresdk "github.com/certimate-go/certimate/pkg/sdk3rd/gcore" ) type CertmgrConfig struct { // G-Core API Token。 ApiToken string `json:"apiToken"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *sslcerts.Service } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.ApiToken) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 新增证书 // REF: https://api.gcore.com/docs/cdn#tag/SSL-certificates/operation/add_ssl_certificates createCertificateReq := &sslcerts.CreateRequest{ Name: fmt.Sprintf("certimate_%d", time.Now().UnixMilli()), Cert: certPEM, PrivateKey: privkeyPEM, Automated: false, ValidateRootCA: false, } createCertificateResp, err := c.sdkClient.Create(ctx, createCertificateReq) c.logger.Debug("sdk request 'sslcerts.Create'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'sslcerts.Create': %w", err) } return &certmgr.UploadResult{ CertId: fmt.Sprintf("%d", createCertificateResp.ID), CertName: createCertificateResp.Name, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func createSDKClient(apiToken string) (*sslcerts.Service, error) { if apiToken == "" { return nil, errors.New("gcore: invalid api token") } requester := gcore.NewClient( gcoresdk.BASE_URL, gcore.WithSigner(gcoresdk.NewAuthRequestSigner(apiToken)), ) service := sslcerts.NewService(requester) return service, nil } ================================================ FILE: pkg/core/certmgr/providers/huaweicloud-elb/huaweicloud_elb.go ================================================ package huaweicloudelb import ( "context" "errors" "fmt" "log/slog" "time" "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic" "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global" hcelb "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3" hcelbmodel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/model" hcelbregion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/region" hciam "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3" hciammodel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model" hciamregion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" "github.com/certimate-go/certimate/pkg/core/certmgr/providers/huaweicloud-elb/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // 华为云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // 华为云企业项目 ID。 EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` // 华为云区域。 Region string `json:"region"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *internal.ElbClient } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 查询已有证书,避免重复上传 // REF: https://support.huaweicloud.com/api-elb/ListCertificates.html listCertificatesMarker := (*string)(nil) for { select { case <-ctx.Done(): return nil, ctx.Err() default: } listCertificatesReq := &hcelbmodel.ListCertificatesRequest{ Marker: listCertificatesMarker, Limit: lo.ToPtr(int32(2000)), Type: lo.ToPtr([]string{"server"}), } listCertificatesResp, err := c.sdkClient.ListCertificates(listCertificatesReq) c.logger.Debug("sdk request 'elb.ListCertificates'", slog.Any("request", listCertificatesReq), slog.Any("response", listCertificatesResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'elb.ListCertificates': %w", err) } if listCertificatesResp.Certificates == nil { break } for _, certItem := range *listCertificatesResp.Certificates { // 如果已存在相同证书,直接返回 if xcert.EqualCertificatesFromPEM(certPEM, certItem.Certificate) { c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: certItem.Id, CertName: certItem.Name, }, nil } } if len(*listCertificatesResp.Certificates) == 0 || listCertificatesResp.PageInfo.NextMarker == nil { break } listCertificatesMarker = listCertificatesResp.PageInfo.NextMarker } // 获取项目 ID // REF: https://support.huaweicloud.com/api-iam/iam_06_0001.html projectId, err := getSDKProjectId(c.config.AccessKeyId, c.config.SecretAccessKey, c.config.Region) if err != nil { return nil, fmt.Errorf("failed to get SDK project id: %w", err) } // 生成新证书名(需符合华为云命名规则) certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) // 创建新证书 // REF: https://support.huaweicloud.com/api-elb/CreateCertificate.html createCertificateReq := &hcelbmodel.CreateCertificateRequest{ Body: &hcelbmodel.CreateCertificateRequestBody{ Certificate: &hcelbmodel.CreateCertificateOption{ EnterpriseProjectId: lo.EmptyableToPtr(c.config.EnterpriseProjectId), ProjectId: lo.ToPtr(projectId), Name: lo.ToPtr(certName), Certificate: lo.ToPtr(certPEM), PrivateKey: lo.ToPtr(privkeyPEM), }, }, } createCertificateResp, err := c.sdkClient.CreateCertificate(createCertificateReq) c.logger.Debug("sdk request 'elb.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'elb.CreateCertificate': %w", err) } return &certmgr.UploadResult{ CertId: createCertificateResp.Certificate.Id, CertName: createCertificateResp.Certificate.Name, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { // 更新证书 // REF: https://support.huaweicloud.com/api-elb/UpdateCertificate.html updateCertificateReq := &hcelbmodel.UpdateCertificateRequest{ CertificateId: certIdOrName, Body: &hcelbmodel.UpdateCertificateRequestBody{ Certificate: &hcelbmodel.UpdateCertificateOption{ Certificate: lo.ToPtr(certPEM), PrivateKey: lo.ToPtr(privkeyPEM), }, }, } updateCertificateResp, err := c.sdkClient.UpdateCertificate(updateCertificateReq) c.logger.Debug("sdk request 'elb.UpdateCertificate'", slog.Any("request", updateCertificateReq), slog.Any("response", updateCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'elb.UpdateCertificate': %w", err) } return &certmgr.OperateResult{}, nil } func createSDKClient(accessKeyId, secretAccessKey, region string) (*internal.ElbClient, error) { if region == "" { region = "cn-north-4" // ELB 服务默认区域:华北四北京 } auth, err := basic.NewCredentialsBuilder(). WithAk(accessKeyId). WithSk(secretAccessKey). SafeBuild() if err != nil { return nil, err } hcRegion, err := hcelbregion.SafeValueOf(region) if err != nil { return nil, err } hcClient, err := hcelb.ElbClientBuilder(). WithRegion(hcRegion). WithCredential(auth). SafeBuild() if err != nil { return nil, err } client := internal.NewElbClient(hcClient) return client, nil } func getSDKProjectId(accessKeyId, secretAccessKey, region string) (string, error) { if region == "" { region = "cn-north-4" // IAM 服务默认区域:华北四北京 } auth, err := global.NewCredentialsBuilder(). WithAk(accessKeyId). WithSk(secretAccessKey). SafeBuild() if err != nil { return "", err } hcRegion, err := hciamregion.SafeValueOf(region) if err != nil { return "", err } hcClient, err := hciam.IamClientBuilder(). WithRegion(hcRegion). WithCredential(auth). SafeBuild() if err != nil { return "", err } client := hciam.NewIamClient(hcClient) request := &hciammodel.KeystoneListProjectsRequest{ Name: ®ion, } response, err := client.KeystoneListProjects(request) if err != nil { return "", err } else if response.Projects == nil || len(*response.Projects) == 0 { return "", errors.New("huaweicloud: no project found") } return (*response.Projects)[0].Id, nil } ================================================ FILE: pkg/core/certmgr/providers/huaweicloud-elb/internal/client.go ================================================ package internal import ( httpclient "github.com/huaweicloud/huaweicloud-sdk-go-v3/core" hwelb "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3" "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/model" ) // This is a partial copy of https://github.com/huaweicloud/huaweicloud-sdk-go-v3/blob/master/services/elb/v3/elb_client.go // to lightweight the vendor packages in the built binary. type ElbClient struct { HcClient *httpclient.HcHttpClient } func NewElbClient(hcClient *httpclient.HcHttpClient) *ElbClient { return &ElbClient{HcClient: hcClient} } func (c *ElbClient) CreateCertificate(request *model.CreateCertificateRequest) (*model.CreateCertificateResponse, error) { requestDef := hwelb.GenReqDefForCreateCertificate() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.CreateCertificateResponse), nil } } func (c *ElbClient) ListCertificates(request *model.ListCertificatesRequest) (*model.ListCertificatesResponse, error) { requestDef := hwelb.GenReqDefForListCertificates() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.ListCertificatesResponse), nil } } func (c *ElbClient) UpdateCertificate(request *model.UpdateCertificateRequest) (*model.UpdateCertificateResponse, error) { requestDef := hwelb.GenReqDefForUpdateCertificate() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.UpdateCertificateResponse), nil } } ================================================ FILE: pkg/core/certmgr/providers/huaweicloud-scm/huaweicloud_scm.go ================================================ package huaweicloudscm import ( "context" "errors" "fmt" "log/slog" "strings" "time" "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic" hcscm "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3" hcscmmodel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3/model" hcscmregion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3/region" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" "github.com/certimate-go/certimate/pkg/core/certmgr/providers/huaweicloud-scm/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // 华为云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // 华为云企业项目 ID。 EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` // 华为云区域。 Region string `json:"region"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *internal.ScmClient } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } // 查询已有证书,避免重复上传 // REF: https://support.huaweicloud.com/api-ccm/ListCertificates.html // REF: https://support.huaweicloud.com/api-ccm/ExportCertificate_0.html listCertificatesLimit := 50 listCertificatesOffset := 0 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } listCertificatesReq := &hcscmmodel.ListCertificatesRequest{ EnterpriseProjectId: lo.EmptyableToPtr(c.config.EnterpriseProjectId), Limit: lo.ToPtr(int32(listCertificatesLimit)), Offset: lo.ToPtr(int32(listCertificatesOffset)), SortDir: lo.ToPtr("DESC"), SortKey: lo.ToPtr("certExpiredTime"), } listCertificatesResp, err := c.sdkClient.ListCertificates(listCertificatesReq) c.logger.Debug("sdk request 'scm.ListCertificates'", slog.Any("request", listCertificatesReq), slog.Any("response", listCertificatesResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'scm.ListCertificates': %w", err) } if listCertificatesResp.Certificates == nil { break } for _, certItem := range *listCertificatesResp.Certificates { // 对比证书通用名称 if !strings.EqualFold(certX509.Subject.CommonName, certItem.Domain) { continue } // 对比证书有效期 if certX509.NotAfter.Local().Format(time.DateTime) != strings.TrimSuffix(certItem.ExpireTime, ".0") { continue } // 对比证书内容 exportCertificateReq := &hcscmmodel.ExportCertificateRequest{ CertificateId: certItem.Id, } exportCertificateResp, err := c.sdkClient.ExportCertificate(exportCertificateReq) c.logger.Debug("sdk request 'scm.ExportCertificate'", slog.Any("request", exportCertificateReq), slog.Any("response", exportCertificateResp)) if err != nil { if exportCertificateResp != nil && exportCertificateResp.HttpStatusCode == 404 { continue } return nil, fmt.Errorf("failed to execute sdk request 'scm.ExportCertificate': %w", err) } else { if !xcert.EqualCertificatesFromPEM(certPEM, lo.FromPtr(exportCertificateResp.Certificate)) { continue } } // 如果以上信息都一致,则视为已存在相同证书,直接返回 c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: certItem.Id, CertName: certItem.Name, }, nil } if len(*listCertificatesResp.Certificates) < listCertificatesLimit { break } listCertificatesOffset += listCertificatesLimit } // 生成新证书名(需符合华为云命名规则) certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) // 上传新证书 // REF: https://support.huaweicloud.com/api-ccm/ImportCertificate.html importCertificateReq := &hcscmmodel.ImportCertificateRequest{ Body: &hcscmmodel.ImportCertificateRequestBody{ EnterpriseProjectId: lo.EmptyableToPtr(c.config.EnterpriseProjectId), Name: certName, Certificate: certPEM, PrivateKey: privkeyPEM, }, } importCertificateResp, err := c.sdkClient.ImportCertificate(importCertificateReq) c.logger.Debug("sdk request 'scm.ImportCertificate'", slog.Any("request", importCertificateReq), slog.Any("response", importCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'scm.ImportCertificate': %w", err) } return &certmgr.UploadResult{ CertId: *importCertificateResp.CertificateId, CertName: certName, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func createSDKClient(accessKeyId, secretAccessKey, region string) (*internal.ScmClient, error) { if region == "" { region = "cn-north-4" // SCM 服务默认区域:华北四北京 } auth, err := basic.NewCredentialsBuilder(). WithAk(accessKeyId). WithSk(secretAccessKey). SafeBuild() if err != nil { return nil, err } hcRegion, err := hcscmregion.SafeValueOf(region) if err != nil { return nil, err } hcClient, err := hcscm.ScmClientBuilder(). WithRegion(hcRegion). WithCredential(auth). SafeBuild() if err != nil { return nil, err } client := internal.NewScmClient(hcClient) return client, nil } ================================================ FILE: pkg/core/certmgr/providers/huaweicloud-scm/internal/client.go ================================================ package internal import ( httpclient "github.com/huaweicloud/huaweicloud-sdk-go-v3/core" hwscm "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3" "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3/model" ) // This is a partial copy of https://github.com/huaweicloud/huaweicloud-sdk-go-v3/blob/master/services/scm/v3/scm_client.go // to lightweight the vendor packages in the built binary. type ScmClient struct { HcClient *httpclient.HcHttpClient } func NewScmClient(hcClient *httpclient.HcHttpClient) *ScmClient { return &ScmClient{HcClient: hcClient} } func (c *ScmClient) ExportCertificate(request *model.ExportCertificateRequest) (*model.ExportCertificateResponse, error) { requestDef := hwscm.GenReqDefForExportCertificate() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.ExportCertificateResponse), nil } } func (c *ScmClient) ImportCertificate(request *model.ImportCertificateRequest) (*model.ImportCertificateResponse, error) { requestDef := hwscm.GenReqDefForImportCertificate() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.ImportCertificateResponse), nil } } func (c *ScmClient) ListCertificates(request *model.ListCertificatesRequest) (*model.ListCertificatesResponse, error) { requestDef := hwscm.GenReqDefForListCertificates() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.ListCertificatesResponse), nil } } ================================================ FILE: pkg/core/certmgr/providers/huaweicloud-waf/huaweicloud_waf.go ================================================ package huaweicloudwaf import ( "context" "errors" "fmt" "log/slog" "time" "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic" "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global" hciam "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3" hciammodel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model" hciamregion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region" hcwaf "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1" hcwafmodel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1/model" hcwafregion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1/region" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" "github.com/certimate-go/certimate/pkg/core/certmgr/providers/huaweicloud-waf/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // 华为云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // 华为云企业项目 ID。 EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` // 华为云区域。 Region string `json:"region"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *internal.WafClient } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 查询已有证书,避免重复上传 // REF: https://support.huaweicloud.com/api-waf/ListCertificates.html // REF: https://support.huaweicloud.com/api-waf/ShowCertificate.html listCertificatesPage := 1 listCertificatesPageSize := 100 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } listCertificatesReq := &hcwafmodel.ListCertificatesRequest{ EnterpriseProjectId: lo.EmptyableToPtr(c.config.EnterpriseProjectId), Page: lo.ToPtr(int32(listCertificatesPage)), Pagesize: lo.ToPtr(int32(listCertificatesPageSize)), } listCertificatesResp, err := c.sdkClient.ListCertificates(listCertificatesReq) c.logger.Debug("sdk request 'waf.ShowCertificate'", slog.Any("request", listCertificatesReq), slog.Any("response", listCertificatesResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'waf.ListCertificates': %w", err) } if listCertificatesResp.Items == nil { break } for _, certItem := range *listCertificatesResp.Items { showCertificateReq := &hcwafmodel.ShowCertificateRequest{ EnterpriseProjectId: lo.EmptyableToPtr(c.config.EnterpriseProjectId), CertificateId: certItem.Id, } showCertificateResp, err := c.sdkClient.ShowCertificate(showCertificateReq) c.logger.Debug("sdk request 'waf.ShowCertificate'", slog.Any("request", showCertificateReq), slog.Any("response", showCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'waf.ShowCertificate': %w", err) } // 如果已存在相同证书,直接返回 if xcert.EqualCertificatesFromPEM(certPEM, lo.FromPtr(showCertificateResp.Content)) { c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: certItem.Id, CertName: certItem.Name, }, nil } } if len(*listCertificatesResp.Items) < listCertificatesPageSize { break } listCertificatesPage++ } // 生成新证书名(需符合华为云命名规则) certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) // 创建证书 // REF: https://support.huaweicloud.com/api-waf/CreateCertificate.html createCertificateReq := &hcwafmodel.CreateCertificateRequest{ EnterpriseProjectId: lo.EmptyableToPtr(c.config.EnterpriseProjectId), Body: &hcwafmodel.CreateCertificateRequestBody{ Name: certName, Content: certPEM, Key: privkeyPEM, }, } createCertificateResp, err := c.sdkClient.CreateCertificate(createCertificateReq) c.logger.Debug("sdk request 'waf.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'waf.CreateCertificate': %w", err) } return &certmgr.UploadResult{ CertId: *createCertificateResp.Id, CertName: certName, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func createSDKClient(accessKeyId, secretAccessKey, region string) (*internal.WafClient, error) { projectId, err := getSDKProjectId(accessKeyId, secretAccessKey, region) if err != nil { return nil, err } auth, err := basic.NewCredentialsBuilder(). WithAk(accessKeyId). WithSk(secretAccessKey). WithProjectId(projectId). SafeBuild() if err != nil { return nil, err } hcRegion, err := hcwafregion.SafeValueOf(region) if err != nil { return nil, err } hcClient, err := hcwaf.WafClientBuilder(). WithRegion(hcRegion). WithCredential(auth). SafeBuild() if err != nil { return nil, err } client := internal.NewWafClient(hcClient) return client, nil } func getSDKProjectId(accessKeyId, secretAccessKey, region string) (string, error) { auth, err := global.NewCredentialsBuilder(). WithAk(accessKeyId). WithSk(secretAccessKey). SafeBuild() if err != nil { return "", err } hcRegion, err := hciamregion.SafeValueOf(region) if err != nil { return "", err } hcClient, err := hciam.IamClientBuilder(). WithRegion(hcRegion). WithCredential(auth). SafeBuild() if err != nil { return "", err } client := hciam.NewIamClient(hcClient) request := &hciammodel.KeystoneListProjectsRequest{ Name: ®ion, } response, err := client.KeystoneListProjects(request) if err != nil { return "", err } else if response.Projects == nil || len(*response.Projects) == 0 { return "", errors.New("huaweicloud: no project found") } return (*response.Projects)[0].Id, nil } ================================================ FILE: pkg/core/certmgr/providers/huaweicloud-waf/internal/client.go ================================================ package internal import ( httpclient "github.com/huaweicloud/huaweicloud-sdk-go-v3/core" hwwaf "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1" "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1/model" ) // This is a partial copy of https://github.com/huaweicloud/huaweicloud-sdk-go-v3/blob/master/services/waf/v1/waf_client.go // to lightweight the vendor packages in the built binary. type WafClient struct { HcClient *httpclient.HcHttpClient } func NewWafClient(hcClient *httpclient.HcHttpClient) *WafClient { return &WafClient{HcClient: hcClient} } func (c *WafClient) CreateCertificate(request *model.CreateCertificateRequest) (*model.CreateCertificateResponse, error) { requestDef := hwwaf.GenReqDefForCreateCertificate() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.CreateCertificateResponse), nil } } func (c *WafClient) ListCertificates(request *model.ListCertificatesRequest) (*model.ListCertificatesResponse, error) { requestDef := hwwaf.GenReqDefForListCertificates() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.ListCertificatesResponse), nil } } func (c *WafClient) ShowCertificate(request *model.ShowCertificateRequest) (*model.ShowCertificateResponse, error) { requestDef := hwwaf.GenReqDefForShowCertificate() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.ShowCertificateResponse), nil } } ================================================ FILE: pkg/core/certmgr/providers/jdcloud-ssl/jdcloud_ssl.go ================================================ package jdcloudssl import ( "context" "crypto/sha256" "encoding/hex" "errors" "fmt" "log/slog" "strings" "time" jdcore "github.com/jdcloud-api/jdcloud-sdk-go/core" jdsslapi "github.com/jdcloud-api/jdcloud-sdk-go/services/ssl/apis" jdsslclient "github.com/jdcloud-api/jdcloud-sdk-go/services/ssl/client" "github.com/certimate-go/certimate/pkg/core/certmgr" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // 京东云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 京东云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *jdsslclient.SslClient } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } // 格式化私钥内容,以便后续计算私钥摘要 privkeyPEM = strings.TrimSpace(privkeyPEM) privkeyPEM = strings.ReplaceAll(privkeyPEM, "\r", "") privkeyPEM = strings.ReplaceAll(privkeyPEM, "\n", "\r\n") privkeyPEM = privkeyPEM + "\r\n" // 查看证书列表 // REF: https://docs.jdcloud.com/cn/ssl-certificate/api/describecerts describeCertsPageNumber := 1 describeCertsPageSize := 10 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } describeCertsReq := jdsslapi.NewDescribeCertsRequestWithoutParam() describeCertsReq.SetDomainName(certX509.Subject.CommonName) describeCertsReq.SetPageNumber(describeCertsPageNumber) describeCertsReq.SetPageSize(describeCertsPageSize) describeCertsResp, err := c.sdkClient.DescribeCerts(describeCertsReq) c.logger.Debug("sdk request 'ssl.DescribeCerts'", slog.Any("request", describeCertsReq), slog.Any("response", describeCertsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'ssl.DescribeCerts': %w", err) } for _, certItem := range describeCertsResp.Result.CertListDetails { // 对比证书通用名称 if !strings.EqualFold(certX509.Subject.CommonName, certItem.CommonName) { continue } // 对比证书多域名 if !strings.EqualFold(strings.Join(certX509.DNSNames, ","), strings.Join(certItem.DnsNames, ",")) { continue } // 对比证书有效期 oldCertNotBefore, _ := time.Parse(time.RFC3339, certItem.StartTime) oldCertNotAfter, _ := time.Parse(time.RFC3339, certItem.EndTime) if !certX509.NotBefore.Equal(oldCertNotBefore) || !certX509.NotAfter.Equal(oldCertNotAfter) { continue } // 对比私钥 SHA-256 摘要 newKeyDigest := sha256.Sum256([]byte(privkeyPEM)) newKeyDigestHex := hex.EncodeToString(newKeyDigest[:]) if !strings.EqualFold(newKeyDigestHex, certItem.Digest) { continue } // 如果以上信息都一致,则视为已存在相同证书,直接返回 c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: certItem.CertId, CertName: certItem.CertName, }, nil } if len(describeCertsResp.Result.CertListDetails) < int(describeCertsPageSize) { break } else { describeCertsPageNumber++ } } // 生成新证书名(需符合京东云命名规则) certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) // 上传证书 // REF: https://docs.jdcloud.com/cn/ssl-certificate/api/uploadcert uploadCertReq := jdsslapi.NewUploadCertRequestWithoutParam() uploadCertReq.SetCertName(certName) uploadCertReq.SetCertFile(certPEM) uploadCertReq.SetKeyFile(privkeyPEM) uploadCertResp, err := c.sdkClient.UploadCert(uploadCertReq) c.logger.Debug("sdk request 'ssl.UploadCertificate'", slog.Any("request", uploadCertReq), slog.Any("response", uploadCertResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'ssl.UploadCertificate': %w", err) } return &certmgr.UploadResult{ CertId: uploadCertResp.Result.CertId, CertName: certName, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func createSDKClient(accessKeyId, accessKeySecret string) (*jdsslclient.SslClient, error) { clientCredentials := jdcore.NewCredentials(accessKeyId, accessKeySecret) client := jdsslclient.NewSslClient(clientCredentials) client.DisableLogger() return client, nil } ================================================ FILE: pkg/core/certmgr/providers/jdcloud-ssl/jdcloud_ssl_test.go ================================================ package jdcloudssl_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/jdcloud-ssl" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string ) func init() { argsPrefix := "JDCLOUDSSL_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") } /* Shell command to run this test: go test -v ./jdcloud_ssl_test.go -args \ --JDCLOUDSSL_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --JDCLOUDSSL_INPUTKEYPATH="/path/to/your-input-key.pem" \ --JDCLOUDSSL_ACCESSKEYID="your-access-key-id" \ --JDCLOUDSSL_ACCESSKEYSECRET="your-access-key-secret" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/certmgr/providers/nginxproxymanager/consts.go ================================================ package nginxproxymanager const ( AUTH_METHOD_PASSWORD = "password" AUTH_METHOD_TOKEN = "token" ) ================================================ FILE: pkg/core/certmgr/providers/nginxproxymanager/nginxproxymanager.go ================================================ package nginxproxymanager import ( "context" "crypto/tls" "errors" "fmt" "log/slog" "strconv" "time" "github.com/certimate-go/certimate/pkg/core/certmgr" npmsdk "github.com/certimate-go/certimate/pkg/sdk3rd/nginxproxymanager" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // NPM 服务地址。 ServerUrl string `json:"serverUrl"` // NPM API 认证方式。 // 可取值 "password"、"token"。 // 零值时默认值 [AUTH_METHOD_PASSWORD]。 AuthMethod string `json:"authMethod,omitempty"` // NPM 用户名。 Username string `json:"username,omitempty"` // NPM 密码。 Password string `json:"password,omitempty"` // NPM API Token。 ApiToken string `json:"apiToken,omitempty"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *npmsdk.Client } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.ServerUrl, config.AuthMethod, config.Username, config.Password, config.ApiToken, config.AllowInsecureConnections) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 提取服务器证书和中间证书 serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM) if err != nil { return nil, fmt.Errorf("failed to extract certs: %w", err) } // 获取全部证书,避免重复上传 listCertificatesReq := &npmsdk.NginxListCertificatesRequest{} listCertificatesResp, err := c.sdkClient.NginxListCertificatesWithContext(ctx, listCertificatesReq) c.logger.Debug("sdk request 'nginx.ListCertificates'", slog.Any("request", listCertificatesReq), slog.Any("response", listCertificatesResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'nginx.ListCertificates': %w", err) } else { for _, certItem := range *listCertificatesResp { if certItem.Meta.Certificate == serverCertPEM && certItem.Meta.CertificateKey == privkeyPEM && certItem.Meta.IntermediateCertificate == intermediaCertPEM { // 如果已存在相同证书,直接返回 c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: fmt.Sprintf("%d", certItem.Id), CertName: certItem.NiceName, }, nil } } } // 创建证书 nginxCreateCertificateReq := &npmsdk.NginxCreateCertificateRequest{ NiceName: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()), Provider: "other", } nginxCreateCertificateResp, err := c.sdkClient.NginxCreateCertificateWithContext(ctx, nginxCreateCertificateReq) c.logger.Debug("sdk request 'nginx.CreateCertificate'", slog.Any("request", nginxCreateCertificateReq), slog.Any("response", nginxCreateCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'nginx.CreateCertificate': %w", err) } // 上传证书文件 ngincxUploadCertificateReq := &npmsdk.NginxUploadCertificateRequest{ CertificateMeta: npmsdk.CertificateMeta{ Certificate: serverCertPEM, CertificateKey: privkeyPEM, IntermediateCertificate: intermediaCertPEM, }, } ngincxUploadCertificateResp, err := c.sdkClient.NginxUploadCertificateWithContext(ctx, nginxCreateCertificateResp.Id, ngincxUploadCertificateReq) c.logger.Debug("sdk request 'nginx.UploadCertificate'", slog.Int64("request.certId", nginxCreateCertificateResp.Id), slog.Any("request", ngincxUploadCertificateReq), slog.Any("response", ngincxUploadCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'nginx.UploadCertificate': %w", err) } return &certmgr.UploadResult{ CertId: fmt.Sprintf("%d", nginxCreateCertificateResp.Id), CertName: nginxCreateCertificateResp.NiceName, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { certId, err := strconv.ParseInt(certIdOrName, 10, 64) if err != nil { return nil, err } // 提取服务器证书和中间证书 serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM) if err != nil { return nil, fmt.Errorf("failed to extract certs: %w", err) } // 上传证书文件 ngincxUploadCertificateReq := &npmsdk.NginxUploadCertificateRequest{ CertificateMeta: npmsdk.CertificateMeta{ Certificate: serverCertPEM, CertificateKey: privkeyPEM, IntermediateCertificate: intermediaCertPEM, }, } ngincxUploadCertificateResp, err := c.sdkClient.NginxUploadCertificateWithContext(ctx, certId, ngincxUploadCertificateReq) c.logger.Debug("sdk request 'nginx.UploadCertificate'", slog.Int64("request.certId", certId), slog.Any("request", ngincxUploadCertificateReq), slog.Any("response", ngincxUploadCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'nginx.UploadCertificate': %w", err) } return &certmgr.OperateResult{}, nil } func createSDKClient(serverUrl, authMethod, username, password, apiToken string, skipTlsVerify bool) (*npmsdk.Client, error) { var client *npmsdk.Client var err error switch authMethod { case "", AUTH_METHOD_PASSWORD: { client, err = npmsdk.NewClient(serverUrl, username, password) } case AUTH_METHOD_TOKEN: { client, err = npmsdk.NewClientWithJwtToken(serverUrl, apiToken) } } if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } ================================================ FILE: pkg/core/certmgr/providers/nginxproxymanager/nginxproxymanager_test.go ================================================ package nginxproxymanager_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/nginxproxymanager" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fUsername string fPassword string ) func init() { argsPrefix := "NGINXPROXYMANAGER_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "") flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "") } /* Shell command to run this test: go test -v ./nginxproxymanager_test.go -args \ --NGINXPROXYMANAGER_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --NGINXPROXYMANAGER_INPUTKEYPATH="/path/to/your-input-key.pem" \ --NGINXPROXYMANAGER_SERVERURL="http://127.0.0.1:81" \ --NGINXPROXYMANAGER_USERNAME="your-username" \ --NGINXPROXYMANAGER_PASSWORD="your-password" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("USERNAME: %v", fUsername), fmt.Sprintf("PASSWORD: %v", fPassword), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ ServerUrl: fServerUrl, AuthMethod: provider.AUTH_METHOD_PASSWORD, Username: fUsername, Password: fPassword, AllowInsecureConnections: true, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/certmgr/providers/qiniu-sslcert/qiniu_sslcert.go ================================================ package qiniusslcert import ( "context" "crypto/x509" "errors" "fmt" "log/slog" "slices" "strings" "time" "github.com/qiniu/go-sdk/v7/auth" "github.com/certimate-go/certimate/pkg/core/certmgr" qiniusdk "github.com/certimate-go/certimate/pkg/sdk3rd/qiniu" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // 七牛云 AccessKey。 AccessKey string `json:"accessKey"` // 七牛云 SecretKey。 SecretKey string `json:"secretKey"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *qiniusdk.SslCertManager } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.AccessKey, config.SecretKey) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } // 生成新证书名(需符合七牛云命名规则) certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) // 查询已有证书,避免重复上传 getSslCertListMarker := "" for { select { case <-ctx.Done(): return nil, ctx.Err() default: } getSslCertListResp, err := c.sdkClient.GetSslCertList(ctx, getSslCertListMarker, 200) c.logger.Debug("sdk request 'sslcert.GetList'", slog.Any("request.marker", getSslCertListMarker), slog.Any("response", getSslCertListResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'sslcert.GetList': %w", err) } for _, sslItem := range getSslCertListResp.Certs { // 对比证书通用名称 if !strings.EqualFold(certX509.Subject.CommonName, sslItem.CommonName) { continue } // 对比证书多域名 if !slices.Equal(certX509.DNSNames, sslItem.DnsNames) { continue } // 对比证书有效期 if certX509.NotBefore.Unix() != sslItem.NotBefore || certX509.NotAfter.Unix() != sslItem.NotAfter { continue } // 对比证书公钥算法 switch certX509.PublicKeyAlgorithm { case x509.RSA: if !strings.EqualFold(sslItem.Encrypt, "RSA") { continue } case x509.ECDSA: if !strings.EqualFold(sslItem.Encrypt, "ECDSA") { continue } case x509.Ed25519: if !strings.EqualFold(sslItem.Encrypt, "Ed25519") { continue } default: // 未知算法,跳过 continue } // 如果以上信息都一致,则视为已存在相同证书,直接返回 c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: sslItem.CertID, CertName: sslItem.Name, }, nil } if len(getSslCertListResp.Certs) == 0 || getSslCertListResp.Marker == "" { break } getSslCertListMarker = getSslCertListResp.Marker } // 上传新证书 // REF: https://developer.qiniu.com/fusion/8593/interface-related-certificate uploadSslCertResp, err := c.sdkClient.UploadSslCert(ctx, certName, certX509.Subject.CommonName, certPEM, privkeyPEM) c.logger.Debug("sdk request 'sslcert.Upload'", slog.Any("response", uploadSslCertResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'sslcert.Upload': %w", err) } return &certmgr.UploadResult{ CertId: uploadSslCertResp.CertID, CertName: certName, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func createSDKClient(accessKey, secretKey string) (*qiniusdk.SslCertManager, error) { if secretKey == "" { return nil, errors.New("qiniu: invalid access key") } if secretKey == "" { return nil, errors.New("qiniu: invalid secret key") } credential := auth.New(accessKey, secretKey) client := qiniusdk.NewSslCertManager(credential) return client, nil } ================================================ FILE: pkg/core/certmgr/providers/qiniu-sslcert/qiniu_sslcert_test.go ================================================ package qiniusslcert_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/qiniu-sslcert" ) var ( fInputCertPath string fInputKeyPath string fAccessKey string fSecretKey string ) func init() { argsPrefix := "QINIUSSLCERT_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "") flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") } /* Shell command to run this test: go test -v ./qiniu_sslcert_test.go -args \ --QINIUSSLCERT_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --QINIUSSLCERT_INPUTKEYPATH="/path/to/your-input-key.pem" \ --QINIUSSLCERT_ACCESSKEY="your-access-key" \ --QINIUSSLCERT_SECRETKEY="your-secret-key" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEY: %v", fAccessKey), fmt.Sprintf("SECRETKEY: %v", fSecretKey), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ AccessKey: fAccessKey, SecretKey: fSecretKey, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/certmgr/providers/rainyun-sslcenter/rainyun_sslcenter.go ================================================ package rainyunsslcenter import ( "context" "errors" "fmt" "log/slog" "strconv" "strings" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" rainyunsdk "github.com/certimate-go/certimate/pkg/sdk3rd/rainyun" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // 雨云 API 密钥。 ApiKey string `json:"ApiKey"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *rainyunsdk.Client } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.ApiKey) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 避免重复上传 if upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM); err != nil { return nil, err } else if upok { c.logger.Info("ssl certificate already exists") return upres, nil } // SSL 证书上传 // REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943046 sslCenterCreateReq := &rainyunsdk.SslCenterCreateRequest{ Cert: certPEM, Key: privkeyPEM, } sslCenterCreateResp, err := c.sdkClient.SslCenterCreateWithContext(ctx, sslCenterCreateReq) c.logger.Debug("sdk request 'sslcenter.Create'", slog.Any("request", sslCenterCreateReq), slog.Any("response", sslCenterCreateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'sslcenter.Create': %w", err) } // 获取刚刚上传证书 ID if upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM); err != nil { return nil, err } else if !upok { return nil, errors.New("could not find ssl certificate, may be upload failed") } else { return upres, nil } } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { certId, err := strconv.ParseInt(certIdOrName, 10, 64) if err != nil { return nil, err } // SSL 证书替换操作 // REF: https://s.apifox.cn/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943049 sslCenterUpdateReq := &rainyunsdk.SslCenterUpdateRequest{ Cert: certPEM, Key: privkeyPEM, } sslCenterUpdateResp, err := c.sdkClient.SslCenterUpdateWithContext(ctx, certId, sslCenterUpdateReq) c.logger.Debug("sdk request 'sslcenter.Update'", slog.Int64("certId", certId), slog.Any("request", sslCenterUpdateReq), slog.Any("response", sslCenterUpdateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'sslcenter.Update': %w", err) } return &certmgr.OperateResult{}, nil } func (c *Certmgr) tryGetResultIfCertExists(ctx context.Context, certPEM string) (*certmgr.UploadResult, bool, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, false, err } // 获取 SSL 证书列表 // REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943046 // REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943048 sslCenterListPage := 1 sslCenterListPerPage := 100 for { select { case <-ctx.Done(): return nil, false, ctx.Err() default: } sslCenterListReq := &rainyunsdk.SslCenterListRequest{ Filters: &rainyunsdk.SslCenterListFilters{ Domain: &certX509.Subject.CommonName, }, Page: lo.ToPtr(int32(sslCenterListPage)), PerPage: lo.ToPtr(int32(sslCenterListPerPage)), } sslCenterListResp, err := c.sdkClient.SslCenterListWithContext(ctx, sslCenterListReq) c.logger.Debug("sdk request 'sslcenter.List'", slog.Any("request", sslCenterListReq), slog.Any("response", sslCenterListResp)) if err != nil { return nil, false, fmt.Errorf("failed to execute sdk request 'sslcenter.List': %w", err) } if sslCenterListResp.Data == nil { break } for _, sslItem := range sslCenterListResp.Data.Records { // 对比证书的多域名 if sslItem.Domain != strings.Join(certX509.DNSNames, ", ") { continue } // 对比证书的有效期 if sslItem.StartDate != certX509.NotBefore.Unix() || sslItem.ExpireDate != certX509.NotAfter.Unix() { continue } // 对比证书内容 sslCenterGetResp, err := c.sdkClient.SslCenterGetWithContext(ctx, sslItem.ID) if err != nil { return nil, false, fmt.Errorf("failed to execute sdk request 'sslcenter.Get': %w", err) } else { if !xcert.EqualCertificatesFromPEM(certPEM, sslCenterGetResp.Data.Cert) { continue } } // 如果以上信息都一致,则视为已存在相同证书,直接返回 return &certmgr.UploadResult{ CertId: fmt.Sprintf("%d", sslItem.ID), }, true, nil } if len(sslCenterListResp.Data.Records) < sslCenterListPerPage { break } sslCenterListPage++ } return nil, false, nil } func createSDKClient(apiKey string) (*rainyunsdk.Client, error) { return rainyunsdk.NewClient(apiKey) } ================================================ FILE: pkg/core/certmgr/providers/rainyun-sslcenter/rainyun_sslcenter_test.go ================================================ package rainyunsslcenter_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/rainyun-sslcenter" ) var ( fInputCertPath string fInputKeyPath string fApiKey string ) func init() { argsPrefix := "RAINYUNSSLCENTER_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") } /* Shell command to run this test: go test -v ./rainyun_sslcenter_test.go -args \ --RAINYUNSSLCENTER_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --RAINYUNSSLCENTER_INPUTKEYPATH="/path/to/your-input-key.pem" \ --RAINYUNSSLCENTER_APIKEY="your-api-key" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("APIKEY: %v", fApiKey), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ ApiKey: fApiKey, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/certmgr/providers/tencentcloud-ssl/internal/client.go ================================================ package internal import ( "context" "errors" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" tcssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" ) // This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/ssl/v20191205/client.go // to lightweight the vendor packages in the built binary. type SslClient struct { common.Client } func NewSslClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *SslClient, err error) { client = &SslClient{} client.Init(region). WithCredential(credential). WithProfile(clientProfile) return } func (c *SslClient) UploadCertificate(request *tcssl.UploadCertificateRequest) (response *tcssl.UploadCertificateResponse, err error) { return c.UploadCertificateWithContext(context.Background(), request) } func (c *SslClient) UploadCertificateWithContext(ctx context.Context, request *tcssl.UploadCertificateRequest) (response *tcssl.UploadCertificateResponse, err error) { if request == nil { request = tcssl.NewUploadCertificateRequest() } c.InitBaseRequest(&request.BaseRequest, "ssl", tcssl.APIVersion, "UploadCertificate") if c.GetCredential() == nil { return nil, errors.New("UploadCertificate require credential") } request.SetContext(ctx) response = tcssl.NewUploadCertificateResponse() err = c.Send(request, response) return } ================================================ FILE: pkg/core/certmgr/providers/tencentcloud-ssl/tencentcloud_ssl.go ================================================ package tencentcloudssl import ( "context" "errors" "fmt" "log/slog" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" tcssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" "github.com/certimate-go/certimate/pkg/core/certmgr" "github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl/internal" ) type CertmgrConfig struct { // 腾讯云 SecretId。 SecretId string `json:"secretId"` // 腾讯云 SecretKey。 SecretKey string `json:"secretKey"` // 腾讯云接口端点。 Endpoint string `json:"endpoint,omitempty"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *internal.SslClient } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 上传新证书 // REF: https://cloud.tencent.com/document/api/400/41665 uploadCertificateReq := tcssl.NewUploadCertificateRequest() uploadCertificateReq.CertificatePublicKey = common.StringPtr(certPEM) uploadCertificateReq.CertificatePrivateKey = common.StringPtr(privkeyPEM) uploadCertificateReq.Repeatable = common.BoolPtr(false) uploadCertificateResp, err := c.sdkClient.UploadCertificate(uploadCertificateReq) c.logger.Debug("sdk request 'ssl.UploadCertificate'", slog.Any("request", uploadCertificateReq), slog.Any("response", uploadCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'ssl.UploadCertificate': %w", err) } return &certmgr.UploadResult{ CertId: *uploadCertificateResp.Response.CertificateId, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func createSDKClient(secretId, secretKey, endpoint string) (*internal.SslClient, error) { credential := common.NewCredential(secretId, secretKey) cpf := profile.NewClientProfile() if endpoint != "" { cpf.HttpProfile.Endpoint = endpoint } client, err := internal.NewSslClient(credential, "", cpf) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/certmgr/providers/tencentcloud-ssl/tencentcloud_ssl_test.go ================================================ package tencentcloudssl_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl" ) var ( fInputCertPath string fInputKeyPath string fSecretId string fSecretKey string ) func init() { argsPrefix := "TENCENTCLOUDSSL_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "") flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") } /* Shell command to run this test: go test -v ./tencentcloud_ssl_test.go -args \ --TENCENTCLOUDSSL_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --TENCENTCLOUDSSL_INPUTKEYPATH="/path/to/your-input-key.pem" \ --TENCENTCLOUDSSL_SECRETID="your-secret-id" \ --TENCENTCLOUDSSL_SECRETKEY="your-secret-key" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SECRETID: %v", fSecretId), fmt.Sprintf("SECRETKEY: %v", fSecretKey), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ SecretId: fSecretId, SecretKey: fSecretKey, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/certmgr/providers/ucloud-ulb/ucloud_ulb.go ================================================ package ucloudulb import ( "context" "errors" "fmt" "log/slog" "strings" "time" "github.com/ucloud/ucloud-sdk-go/ucloud" "github.com/ucloud/ucloud-sdk-go/ucloud/auth" "github.com/certimate-go/certimate/pkg/core/certmgr" ucloudsdk "github.com/certimate-go/certimate/pkg/sdk3rd/ucloud/ulb" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // 优刻得 API 私钥。 PrivateKey string `json:"privateKey"` // 优刻得 API 公钥。 PublicKey string `json:"publicKey"` // 优刻得项目 ID。 ProjectId string `json:"projectId,omitempty"` // 优刻得地域。 Region string `json:"region"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *ucloudsdk.ULBClient } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.PrivateKey, config.PublicKey, config.ProjectId, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 避免重复上传 if upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM, privkeyPEM); err != nil { return nil, err } else if upok { c.logger.Info("ssl certificate already exists") return upres, nil } // 提取服务器证书和中间证书 serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM) if err != nil { return nil, fmt.Errorf("failed to extract certs: %w", err) } // 生成新证书名(需符合优刻得命名规则) certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) // 创建 SSL 证书 // REF: https://docs.ucloud.cn/api/ulb-api/create_ssl createSSLReq := c.sdkClient.NewCreateSSLRequest() createSSLReq.SSLName = ucloud.String(certName) createSSLReq.SSLType = ucloud.String("Pem") createSSLReq.UserCert = ucloud.String(serverCertPEM) createSSLReq.CaCert = ucloud.String(intermediaCertPEM) createSSLReq.PrivateKey = ucloud.String(privkeyPEM) createSSLResp, err := c.sdkClient.CreateSSL(createSSLReq) c.logger.Debug("sdk request 'ulb.CreateSSL'", slog.Any("request", createSSLReq), slog.Any("response", createSSLResp)) return &certmgr.UploadResult{ CertId: createSSLResp.SSLId, CertName: certName, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func (c *Certmgr) tryGetResultIfCertExists(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, bool, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, false, err } // 获取 SSL 证书信息 // REF: https://docs.ucloud.cn/api/ulb-api/describe_ssl describeSSLOffset := 0 describeSSLLimit := 100 for { select { case <-ctx.Done(): return nil, false, ctx.Err() default: } describeSSLReq := c.sdkClient.NewDescribeSSLRequest() describeSSLReq.Offset = ucloud.Int(describeSSLOffset) describeSSLReq.Limit = ucloud.Int(describeSSLLimit) describeSSLResp, err := c.sdkClient.DescribeSSL(describeSSLReq) c.logger.Debug("sdk request 'ulb.DescribeSSL'", slog.Any("request", describeSSLReq), slog.Any("response", describeSSLResp)) if err != nil { return nil, false, fmt.Errorf("failed to execute sdk request 'ulb.DescribeSSL': %w", err) } for _, sslItem := range describeSSLResp.DataSet { // 对比证书有效期 if int64(sslItem.NotBefore) != certX509.NotBefore.Unix() || int64(sslItem.NotAfter) != certX509.NotAfter.Unix() { continue } // 对比证书及私钥内容 // 按照“网站证书、私钥、中间证书”的方式拼接 serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM) if err != nil { continue } else { oldSSLContent := sslItem.SSLContent oldSSLContent = strings.ReplaceAll(oldSSLContent, "\r", "") oldSSLContent = strings.ReplaceAll(oldSSLContent, "\n", "") oldSSLContent = strings.ReplaceAll(oldSSLContent, "\t", "") oldSSLContent = strings.ReplaceAll(oldSSLContent, " ", "") newSSLContent := serverCertPEM + privkeyPEM + intermediaCertPEM newSSLContent = strings.ReplaceAll(newSSLContent, "\r", "") newSSLContent = strings.ReplaceAll(newSSLContent, "\n", "") newSSLContent = strings.ReplaceAll(newSSLContent, "\t", "") newSSLContent = strings.ReplaceAll(newSSLContent, " ", "") if oldSSLContent != newSSLContent { continue } } // 如果以上信息都一致,则视为已存在相同证书,直接返回 return &certmgr.UploadResult{ CertId: sslItem.SSLId, CertName: sslItem.SSLName, }, true, nil } if len(describeSSLResp.DataSet) < describeSSLLimit { break } describeSSLOffset += describeSSLLimit } return nil, false, nil } func createSDKClient(privateKey, publicKey, projectId, region string) (*ucloudsdk.ULBClient, error) { if privateKey == "" { return nil, fmt.Errorf("ucloud: invalid private key") } if publicKey == "" { return nil, fmt.Errorf("ucloud: invalid public key") } cfg := ucloud.NewConfig() cfg.ProjectId = projectId cfg.Region = region credential := auth.NewCredential() credential.PrivateKey = privateKey credential.PublicKey = publicKey client := ucloudsdk.NewClient(&cfg, &credential) return client, nil } ================================================ FILE: pkg/core/certmgr/providers/ucloud-ulb/ucloud_ulb_test.go ================================================ package ucloudulb_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ucloud-ulb" ) var ( fInputCertPath string fInputKeyPath string fPrivateKey string fPublicKey string fRegion string ) func init() { argsPrefix := "UCLOUDULB_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fPrivateKey, argsPrefix+"PRIVATEKEY", "", "") flag.StringVar(&fPublicKey, argsPrefix+"PUBLICKEY", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") } /* Shell command to run this test: go test -v ./ucloud_ulb_test.go -args \ --UCLOUDULB_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --UCLOUDULB_INPUTKEYPATH="/path/to/your-input-key.pem" \ --UCLOUDULB_PRIVATEKEY="your-private-key" \ --UCLOUDULB_PUBLICKEY="your-public-key" \ --UCLOUDULB_REGION="cn-bj2" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("PRIVATEKEY: %v", fPrivateKey), fmt.Sprintf("PUBLICKEY: %v", fPublicKey), fmt.Sprintf("REGION: %v", fRegion), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ PrivateKey: fPrivateKey, PublicKey: fPublicKey, Region: fRegion, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/certmgr/providers/ucloud-upathx/ucloud_upathx.go ================================================ package ucloudulb import ( "context" "errors" "fmt" "log/slog" "strings" "time" "github.com/ucloud/ucloud-sdk-go/services/uaccount" "github.com/ucloud/ucloud-sdk-go/ucloud" "github.com/ucloud/ucloud-sdk-go/ucloud/auth" "github.com/certimate-go/certimate/pkg/core/certmgr" ucloudsdk "github.com/certimate-go/certimate/pkg/sdk3rd/ucloud/upathx" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // 优刻得 API 私钥。 PrivateKey string `json:"privateKey"` // 优刻得 API 公钥。 PublicKey string `json:"publicKey"` // 优刻得项目 ID。 ProjectId string `json:"projectId,omitempty"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *ucloudsdk.UPathXClient } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.PrivateKey, config.PublicKey, config.ProjectId) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 避免重复上传 if upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM, privkeyPEM); err != nil { return nil, err } else if upok { c.logger.Info("ssl certificate already exists") return upres, nil } // 提取服务器证书和中间证书 serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM) if err != nil { return nil, fmt.Errorf("failed to extract certs: %w", err) } // 生成新证书名(需符合优刻得命名规则) certName := fmt.Sprintf("certimate_%d", time.Now().UnixMilli()) // 创建证书 // REF: https://docs.ucloud.cn/api/pathx-api/create_path_xssl createPathXSSLReq := c.sdkClient.NewCreatePathXSSLRequest() createPathXSSLReq.SSLName = ucloud.String(certName) createPathXSSLReq.SSLType = ucloud.String("Pem") createPathXSSLReq.UserCert = ucloud.String(serverCertPEM) createPathXSSLReq.CACert = ucloud.String(intermediaCertPEM) createPathXSSLReq.PrivateKey = ucloud.String(privkeyPEM) createPathXSSLResp, err := c.sdkClient.CreatePathXSSL(createPathXSSLReq) c.logger.Debug("sdk request 'pathx.CreatePathXSSL'", slog.Any("request", createPathXSSLReq), slog.Any("response", createPathXSSLResp)) return &certmgr.UploadResult{ CertId: createPathXSSLResp.SSLId, CertName: certName, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func (c *Certmgr) tryGetResultIfCertExists(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, bool, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, false, err } // 获取证书信息 // REF: https://docs.ucloud.cn/api/pathx-api/describe_path_xssl describePathXSSLOffset := 0 describePathXSSLLimit := 100 for { select { case <-ctx.Done(): return nil, false, ctx.Err() default: } describePathXSSLReq := c.sdkClient.NewDescribePathXSSLRequest() describePathXSSLReq.Offset = ucloud.Int(describePathXSSLOffset) describePathXSSLReq.Limit = ucloud.Int(describePathXSSLLimit) describePathXSSLResp, err := c.sdkClient.DescribePathXSSL(describePathXSSLReq) c.logger.Debug("sdk request 'pathx.DescribePathXSSL'", slog.Any("request", describePathXSSLReq), slog.Any("response", describePathXSSLResp)) if err != nil { return nil, false, fmt.Errorf("failed to execute sdk request 'pathx.DescribePathXSSL': %w", err) } for _, sslItem := range describePathXSSLResp.DataSet { // 对比证书有效期 if int64(sslItem.ExpireTime) != certX509.NotAfter.Unix() { continue } // 对比证书及私钥内容 // 按照“私钥、证书链”的方式拼接 serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM) if err != nil { continue } else { oldSSLContent := sslItem.SSLContent oldSSLContent = strings.ReplaceAll(oldSSLContent, "\r", "") oldSSLContent = strings.ReplaceAll(oldSSLContent, "\n", "") oldSSLContent = strings.ReplaceAll(oldSSLContent, "\t", "") oldSSLContent = strings.ReplaceAll(oldSSLContent, " ", "") newSSLContent := privkeyPEM + serverCertPEM + intermediaCertPEM newSSLContent = strings.ReplaceAll(newSSLContent, "\r", "") newSSLContent = strings.ReplaceAll(newSSLContent, "\n", "") newSSLContent = strings.ReplaceAll(newSSLContent, "\t", "") newSSLContent = strings.ReplaceAll(newSSLContent, " ", "") if oldSSLContent != newSSLContent { continue } } // 如果以上信息都一致,则视为已存在相同证书,直接返回 return &certmgr.UploadResult{ CertId: sslItem.SSLId, CertName: sslItem.SSLName, }, true, nil } if len(describePathXSSLResp.DataSet) < describePathXSSLLimit { break } describePathXSSLOffset += describePathXSSLLimit } return nil, false, nil } func createSDKClient(privateKey, publicKey, projectId string) (*ucloudsdk.UPathXClient, error) { if privateKey == "" { return nil, errors.New("ucloud: invalid private key") } if publicKey == "" { return nil, errors.New("ucloud: invalid public key") } cfg := ucloud.NewConfig() cfg.ProjectId = projectId // PathX 相关接口要求必传 ProjectId 参数 if cfg.ProjectId == "" { defaultProjectId, err := getSDKDefaultProjectId(privateKey, publicKey) if err != nil { return nil, err } cfg.ProjectId = defaultProjectId } credential := auth.NewCredential() credential.PrivateKey = privateKey credential.PublicKey = publicKey client := ucloudsdk.NewClient(&cfg, &credential) return client, nil } func getSDKDefaultProjectId(privateKey, publicKey string) (string, error) { cfg := ucloud.NewConfig() credential := auth.NewCredential() credential.PrivateKey = privateKey credential.PublicKey = publicKey client := uaccount.NewClient(&cfg, &credential) request := client.NewGetProjectListRequest() response, err := client.GetProjectList(request) if err != nil { return "", err } for _, projectItem := range response.ProjectSet { if projectItem.IsDefault { return projectItem.ProjectId, nil } } return "", errors.New("ucloud: no default project found") } ================================================ FILE: pkg/core/certmgr/providers/ucloud-upathx/ucloud_upathx_test.go ================================================ package ucloudulb_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ucloud-upathx" ) var ( fInputCertPath string fInputKeyPath string fPrivateKey string fPublicKey string ) func init() { argsPrefix := "UCLOUDUPATHX_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fPrivateKey, argsPrefix+"PRIVATEKEY", "", "") flag.StringVar(&fPublicKey, argsPrefix+"PUBLICKEY", "", "") } /* Shell command to run this test: go test -v ./ucloud_upathx_test.go -args \ --UCLOUDUPATHX_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --UCLOUDUPATHX_INPUTKEYPATH="/path/to/your-input-key.pem" \ --UCLOUDUPATHX_PRIVATEKEY="your-private-key" \ --UCLOUDUPATHX_PUBLICKEY="your-public-key" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("PRIVATEKEY: %v", fPrivateKey), fmt.Sprintf("PUBLICKEY: %v", fPublicKey), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ PrivateKey: fPrivateKey, PublicKey: fPublicKey, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/certmgr/providers/ucloud-ussl/ucloud_ussl.go ================================================ package ucloudussl import ( "context" "crypto/md5" "crypto/x509" "encoding/base64" "encoding/hex" "errors" "fmt" "log/slog" "strings" "time" "github.com/ucloud/ucloud-sdk-go/ucloud" "github.com/ucloud/ucloud-sdk-go/ucloud/auth" "github.com/certimate-go/certimate/pkg/core/certmgr" ucloudsdk "github.com/certimate-go/certimate/pkg/sdk3rd/ucloud/ussl" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // 优刻得 API 私钥。 PrivateKey string `json:"privateKey"` // 优刻得 API 公钥。 PublicKey string `json:"publicKey"` // 优刻得项目 ID。 ProjectId string `json:"projectId,omitempty"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *ucloudsdk.USSLClient } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.PrivateKey, config.PublicKey, config.ProjectId) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 生成新证书名(需符合优刻得命名规则) certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) // 生成优刻得所需的证书参数 certPEMBase64 := base64.StdEncoding.EncodeToString([]byte(certPEM)) privkeyPEMBase64 := base64.StdEncoding.EncodeToString([]byte(privkeyPEM)) certMd5 := md5.Sum([]byte(certPEMBase64 + privkeyPEMBase64)) certMd5Hex := hex.EncodeToString(certMd5[:]) // 上传托管证书 // REF: https://docs.ucloud.cn/api/usslcertificate-api/upload_normal_certificate uploadNormalCertificateReq := c.sdkClient.NewUploadNormalCertificateRequest() uploadNormalCertificateReq.CertificateName = ucloud.String(certName) uploadNormalCertificateReq.SslPublicKey = ucloud.String(certPEMBase64) uploadNormalCertificateReq.SslPrivateKey = ucloud.String(privkeyPEMBase64) uploadNormalCertificateReq.SslMD5 = ucloud.String(certMd5Hex) uploadNormalCertificateResp, err := c.sdkClient.UploadNormalCertificate(uploadNormalCertificateReq) c.logger.Debug("sdk request 'ussl.UploadNormalCertificate'", slog.Any("request", uploadNormalCertificateReq), slog.Any("response", uploadNormalCertificateResp)) if err != nil { if uploadNormalCertificateResp != nil && uploadNormalCertificateResp.GetRetCode() == 80035 { if upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM); err != nil { return nil, err } else if !upok { return nil, errors.New("could not find ssl certificate, may be upload failed") } else { c.logger.Info("ssl certificate already exists") return upres, nil } } return nil, fmt.Errorf("failed to execute sdk request 'ussl.UploadNormalCertificate': %w", err) } return &certmgr.UploadResult{ CertId: fmt.Sprintf("%d", uploadNormalCertificateResp.CertificateID), CertName: certName, ExtendedData: map[string]any{ "ResourceId": uploadNormalCertificateResp.LongResourceID, }, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func (c *Certmgr) tryGetResultIfCertExists(ctx context.Context, certPEM string) (*certmgr.UploadResult, bool, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, false, err } // 查询用户证书列表 // REF: https://docs.ucloud.cn/api/usslcertificate-api/get_certificate_list // REF: https://docs.ucloud.cn/api/usslcertificate-api/download_certificate getCertificateListPage := 1 getCertificateListLimit := 1000 for { select { case <-ctx.Done(): return nil, false, ctx.Err() default: } getCertificateListReq := c.sdkClient.NewGetCertificateListRequest() getCertificateListReq.Mode = ucloud.String("trust") getCertificateListReq.Domain = ucloud.String(certX509.Subject.CommonName) getCertificateListReq.Sort = ucloud.String("2") getCertificateListReq.Page = ucloud.Int(getCertificateListPage) getCertificateListReq.PageSize = ucloud.Int(getCertificateListLimit) getCertificateListResp, err := c.sdkClient.GetCertificateList(getCertificateListReq) c.logger.Debug("sdk request 'ussl.GetCertificateList'", slog.Any("request", getCertificateListReq), slog.Any("response", getCertificateListResp)) if err != nil { return nil, false, fmt.Errorf("failed to execute sdk request 'ussl.GetCertificateList': %w", err) } for _, certItem := range getCertificateListResp.CertificateList { // 优刻得未提供可唯一标识证书的字段,只能通过多个字段尝试对比来判断是否为同一证书 // 先分别对比证书的多域名、品牌、有效期,再对比签名算法 if len(certX509.DNSNames) == 0 || certItem.Domains != strings.Join(certX509.DNSNames, ",") { continue } if len(certX509.Issuer.Organization) == 0 || certItem.Brand != certX509.Issuer.Organization[0] { continue } if int64(certItem.NotBefore) != certX509.NotBefore.UnixMilli() || int64(certItem.NotAfter) != certX509.NotAfter.UnixMilli() { continue } getCertificateDetailInfoReq := c.sdkClient.NewGetCertificateDetailInfoRequest() getCertificateDetailInfoReq.CertificateID = ucloud.Int(certItem.CertificateID) getCertificateDetailInfoResp, err := c.sdkClient.GetCertificateDetailInfo(getCertificateDetailInfoReq) if err != nil { return nil, false, fmt.Errorf("failed to execute sdk request 'ussl.GetCertificateDetailInfo': %w", err) } switch certX509.SignatureAlgorithm { case x509.SHA256WithRSA: if !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, "SHA256-RSA") { continue } case x509.SHA384WithRSA: if !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, "SHA384-RSA") { continue } case x509.SHA512WithRSA: if !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, "SHA512-RSA") { continue } case x509.SHA256WithRSAPSS: if !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, "SHA256-RSAPSS") { continue } case x509.SHA384WithRSAPSS: if !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, "SHA384-RSAPSS") { continue } case x509.SHA512WithRSAPSS: if !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, "SHA512-RSAPSS") { continue } case x509.ECDSAWithSHA256: if !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, "ECDSA-SHA256") { continue } case x509.ECDSAWithSHA384: if !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, "ECDSA-SHA384") { continue } case x509.ECDSAWithSHA512: if !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, "ECDSA-SHA512") { continue } default: // 未知签名算法,跳过 continue } return &certmgr.UploadResult{ CertId: fmt.Sprintf("%d", certItem.CertificateID), CertName: certItem.Name, ExtendedData: map[string]any{ "ResourceId": certItem.CertificateSN, }, }, true, nil } if len(getCertificateListResp.CertificateList) < getCertificateListLimit { break } getCertificateListPage++ } return nil, false, nil } func createSDKClient(privateKey, publicKey, projectId string) (*ucloudsdk.USSLClient, error) { if privateKey == "" { return nil, fmt.Errorf("ucloud: invalid private key") } if publicKey == "" { return nil, fmt.Errorf("ucloud: invalid public key") } cfg := ucloud.NewConfig() cfg.ProjectId = projectId credential := auth.NewCredential() credential.PrivateKey = privateKey credential.PublicKey = publicKey client := ucloudsdk.NewClient(&cfg, &credential) return client, nil } ================================================ FILE: pkg/core/certmgr/providers/ucloud-ussl/ucloud_ussl_test.go ================================================ package ucloudussl_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ucloud-ussl" ) var ( fInputCertPath string fInputKeyPath string fPrivateKey string fPublicKey string ) func init() { argsPrefix := "UCLOUDUSSL_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fPrivateKey, argsPrefix+"PRIVATEKEY", "", "") flag.StringVar(&fPublicKey, argsPrefix+"PUBLICKEY", "", "") } /* Shell command to run this test: go test -v ./ucloud_ussl_test.go -args \ --UCLOUDUSSL_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --UCLOUDUSSL_INPUTKEYPATH="/path/to/your-input-key.pem" \ --UCLOUDUSSL_PRIVATEKEY="your-private-key" \ --UCLOUDUSSL_PUBLICKEY="your-public-key" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("PRIVATEKEY: %v", fPrivateKey), fmt.Sprintf("PUBLICKEY: %v", fPublicKey), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ PrivateKey: fPrivateKey, PublicKey: fPublicKey, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/certmgr/providers/upyun-ssl/upyun_ssl.go ================================================ package upyunssl import ( "context" "errors" "fmt" "log/slog" "github.com/certimate-go/certimate/pkg/core/certmgr" upyunsdk "github.com/certimate-go/certimate/pkg/sdk3rd/upyun/console" ) type CertmgrConfig struct { // 又拍云账号用户名。 Username string `json:"username"` // 又拍云账号密码。 Password string `json:"password"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *upyunsdk.Client } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.Username, config.Password) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 上传证书 uploadHttpsCertificateReq := &upyunsdk.UploadHttpsCertificateRequest{ Certificate: certPEM, PrivateKey: privkeyPEM, } uploadHttpsCertificateResp, err := c.sdkClient.UploadHttpsCertificateWithContext(ctx, uploadHttpsCertificateReq) c.logger.Debug("sdk request 'console.UploadHttpsCertificate'", slog.Any("response", uploadHttpsCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'console.UploadHttpsCertificate': %w", err) } return &certmgr.UploadResult{ CertId: uploadHttpsCertificateResp.Data.Result.CertificateId, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func createSDKClient(username, password string) (*upyunsdk.Client, error) { return upyunsdk.NewClient(username, password) } ================================================ FILE: pkg/core/certmgr/providers/upyun-ssl/upyun_ssl_test.go ================================================ package upyunssl_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/upyun-ssl" ) var ( fInputCertPath string fInputKeyPath string fUsername string fPassword string ) func init() { argsPrefix := "UPYUNSSL_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "") flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "") } /* Shell command to run this test: go test -v ./upyun_ssl_test.go -args \ --UPYUNSSL_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --UPYUNSSL_INPUTKEYPATH="/path/to/your-input-key.pem" \ --UPYUNSSL_USERNAME="your-username" \ --UPYUNSSL_PASSWORD="your-password" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("USERNAME: %v", fUsername), fmt.Sprintf("PASSWORD: %v", fPassword), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ Username: fUsername, Password: fPassword, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/certmgr/providers/volcengine-cdn/internal/client.go ================================================ package internal import ( "github.com/volcengine/volcengine-go-sdk/service/cdn" "github.com/volcengine/volcengine-go-sdk/volcengine" "github.com/volcengine/volcengine-go-sdk/volcengine/client" "github.com/volcengine/volcengine-go-sdk/volcengine/client/metadata" "github.com/volcengine/volcengine-go-sdk/volcengine/corehandlers" "github.com/volcengine/volcengine-go-sdk/volcengine/request" "github.com/volcengine/volcengine-go-sdk/volcengine/signer/volc" "github.com/volcengine/volcengine-go-sdk/volcengine/volcenginequery" ) // This is a partial copy of https://github.com/volcengine/volcengine-go-sdk/blob/master/service/cdn/service_cdn.go // to lightweight the vendor packages in the built binary. type CdnClient struct { *client.Client } func NewCdnClient(p client.ConfigProvider, cfgs ...*volcengine.Config) *CdnClient { c := p.ClientConfig(cdn.EndpointsID, cfgs...) return newCdnClient(*c.Config, c.Handlers, c.Endpoint, c.SigningRegion, c.SigningName) } func newCdnClient(cfg volcengine.Config, handlers request.Handlers, endpoint, signingRegion, signingName string) *CdnClient { svc := &CdnClient{ Client: client.New( cfg, metadata.ClientInfo{ ServiceName: cdn.ServiceName, ServiceID: cdn.ServiceID, SigningName: signingName, SigningRegion: signingRegion, Endpoint: endpoint, APIVersion: "2021-03-01", }, handlers, ), } svc.Handlers.Build.PushBackNamed(corehandlers.SDKVersionUserAgentHandler) svc.Handlers.Build.PushBackNamed(corehandlers.AddHostExecEnvUserAgentHandler) svc.Handlers.Sign.PushBackNamed(volc.SignRequestHandler) svc.Handlers.Build.PushBackNamed(volcenginequery.BuildHandler) svc.Handlers.Unmarshal.PushBackNamed(volcenginequery.UnmarshalHandler) svc.Handlers.UnmarshalMeta.PushBackNamed(volcenginequery.UnmarshalMetaHandler) svc.Handlers.UnmarshalError.PushBackNamed(volcenginequery.UnmarshalErrorHandler) return svc } func (c *CdnClient) newRequest(op *request.Operation, params, data interface{}) *request.Request { req := c.NewRequest(op, params, data) return req } func (c *CdnClient) AddCertificate(input *cdn.AddCertificateInput) (*cdn.AddCertificateOutput, error) { req, out := c.AddCertificateRequest(input) return out, req.Send() } func (c *CdnClient) AddCertificateRequest(input *cdn.AddCertificateInput) (req *request.Request, output *cdn.AddCertificateOutput) { op := &request.Operation{ Name: "AddCertificate", HTTPMethod: "POST", HTTPPath: "/", } if input == nil { input = &cdn.AddCertificateInput{} } output = &cdn.AddCertificateOutput{} req = c.newRequest(op, input, output) req.HTTPRequest.Header.Set("Content-Type", "application/json; charset=utf-8") return } func (c *CdnClient) ListCertInfo(input *cdn.ListCertInfoInput) (*cdn.ListCertInfoOutput, error) { req, out := c.ListCertInfoRequest(input) return out, req.Send() } func (c *CdnClient) ListCertInfoRequest(input *cdn.ListCertInfoInput) (req *request.Request, output *cdn.ListCertInfoOutput) { op := &request.Operation{ Name: "ListCertInfo", HTTPMethod: "POST", HTTPPath: "/", } if input == nil { input = &cdn.ListCertInfoInput{} } output = &cdn.ListCertInfoOutput{} req = c.newRequest(op, input, output) req.HTTPRequest.Header.Set("Content-Type", "application/json; charset=utf-8") return } ================================================ FILE: pkg/core/certmgr/providers/volcengine-cdn/volcengine_cdn.go ================================================ package volcenginecdn import ( "context" "crypto/sha1" "crypto/sha256" "encoding/hex" "errors" "fmt" "log/slog" "strings" "time" vecdn "github.com/volcengine/volcengine-go-sdk/service/cdn" ve "github.com/volcengine/volcengine-go-sdk/volcengine" vesession "github.com/volcengine/volcengine-go-sdk/volcengine/session" "github.com/certimate-go/certimate/pkg/core/certmgr" "github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-cdn/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // 火山引擎 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 火山引擎 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *internal.CdnClient } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } // 查询证书列表,避免重复上传 // REF: https://www.volcengine.com/docs/6454/125709 listCertInfoPageNum := 1 listCertInfoPageSize := 100 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } listCertInfoReq := &vecdn.ListCertInfoInput{ Source: ve.String("volc_cert_center"), PageNum: ve.Int32(int32(listCertInfoPageNum)), PageSize: ve.Int32(int32(listCertInfoPageSize)), } listCertInfoResp, err := c.sdkClient.ListCertInfo(listCertInfoReq) c.logger.Debug("sdk request 'cdn.ListCertInfo'", slog.Any("request", listCertInfoReq), slog.Any("response", listCertInfoResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.ListCertInfo': %w", err) } for _, certItem := range listCertInfoResp.CertInfo { // 对比证书 SHA-1 摘要 fingerprintSha1 := sha1.Sum(certX509.Raw) if !strings.EqualFold(hex.EncodeToString(fingerprintSha1[:]), ve.StringValue(certItem.CertFingerprint.Sha1)) { continue } // 对比证书 SHA-256 摘要 fingerprintSha256 := sha256.Sum256(certX509.Raw) if !strings.EqualFold(hex.EncodeToString(fingerprintSha256[:]), ve.StringValue(certItem.CertFingerprint.Sha256)) { continue } // 如果以上信息都一致,则视为已存在相同证书,直接返回 c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: ve.StringValue(certItem.CertId), CertName: ve.StringValue(certItem.Desc), }, nil } if len(listCertInfoResp.CertInfo) < listCertInfoPageSize { break } listCertInfoPageNum++ } // 生成新证书名(需符合火山引擎命名规则) certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) // 上传新证书 // REF: https://www.volcengine.com/docs/6454/1245763 addCertificateReq := &vecdn.AddCertificateInput{ Source: ve.String("volc_cert_center"), Certificate: ve.String(certPEM), PrivateKey: ve.String(privkeyPEM), Desc: ve.String(certName), } addCertificateResp, err := c.sdkClient.AddCertificate(addCertificateReq) c.logger.Debug("sdk request 'cdn.AddCertificate'", slog.Any("request", addCertificateResp), slog.Any("response", addCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.AddCertificate': %w", err) } return &certmgr.UploadResult{ CertId: ve.StringValue(addCertificateResp.CertId), CertName: certName, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func createSDKClient(accessKeyId, accessKeySecret string) (*internal.CdnClient, error) { config := ve.NewConfig(). WithAkSk(accessKeyId, accessKeySecret). WithRegion("cn-north-1") session, err := vesession.NewSession(config) if err != nil { return nil, err } client := internal.NewCdnClient(session) return client, nil } ================================================ FILE: pkg/core/certmgr/providers/volcengine-certcenter/internal/client.go ================================================ package internal import ( "github.com/volcengine/volcengine-go-sdk/service/certificateservice" "github.com/volcengine/volcengine-go-sdk/volcengine" "github.com/volcengine/volcengine-go-sdk/volcengine/client" "github.com/volcengine/volcengine-go-sdk/volcengine/client/metadata" "github.com/volcengine/volcengine-go-sdk/volcengine/corehandlers" "github.com/volcengine/volcengine-go-sdk/volcengine/request" "github.com/volcengine/volcengine-go-sdk/volcengine/signer/volc" "github.com/volcengine/volcengine-go-sdk/volcengine/volcenginequery" ) // This is a partial copy of https://github.com/volcengine/volcengine-go-sdk/blob/master/service/certificateservice/service_certificateservice.go // to lightweight the vendor packages in the built binary. type CertificateserviceClient struct { *client.Client } func NewCertificateserviceClient(p client.ConfigProvider, cfgs ...*volcengine.Config) *CertificateserviceClient { c := p.ClientConfig(certificateservice.EndpointsID, cfgs...) return newCertificateserviceClient(*c.Config, c.Handlers, c.Endpoint, c.SigningRegion, c.SigningName) } func newCertificateserviceClient(cfg volcengine.Config, handlers request.Handlers, endpoint, signingRegion, signingName string) *CertificateserviceClient { svc := &CertificateserviceClient{ Client: client.New( cfg, metadata.ClientInfo{ ServiceName: certificateservice.ServiceName, ServiceID: certificateservice.ServiceID, SigningName: signingName, SigningRegion: signingRegion, Endpoint: endpoint, APIVersion: "2024-10-01", }, handlers, ), } svc.Handlers.Build.PushBackNamed(corehandlers.SDKVersionUserAgentHandler) svc.Handlers.Build.PushBackNamed(corehandlers.AddHostExecEnvUserAgentHandler) svc.Handlers.Sign.PushBackNamed(volc.SignRequestHandler) svc.Handlers.Build.PushBackNamed(volcenginequery.BuildHandler) svc.Handlers.Unmarshal.PushBackNamed(volcenginequery.UnmarshalHandler) svc.Handlers.UnmarshalMeta.PushBackNamed(volcenginequery.UnmarshalMetaHandler) svc.Handlers.UnmarshalError.PushBackNamed(volcenginequery.UnmarshalErrorHandler) return svc } func (c *CertificateserviceClient) newRequest(op *request.Operation, params, data interface{}) *request.Request { req := c.NewRequest(op, params, data) return req } func (c *CertificateserviceClient) ImportCertificate(input *certificateservice.ImportCertificateInput) (*certificateservice.ImportCertificateOutput, error) { req, out := c.ImportCertificateRequest(input) return out, req.Send() } func (c *CertificateserviceClient) ImportCertificateRequest(input *certificateservice.ImportCertificateInput) (req *request.Request, output *certificateservice.ImportCertificateOutput) { op := &request.Operation{ Name: "ImportCertificate", HTTPMethod: "POST", HTTPPath: "/", } if input == nil { input = &certificateservice.ImportCertificateInput{} } output = &certificateservice.ImportCertificateOutput{} req = c.newRequest(op, input, output) req.HTTPRequest.Header.Set("Content-Type", "application/json; charset=utf-8") return } ================================================ FILE: pkg/core/certmgr/providers/volcengine-certcenter/volcengine_certcenter.go ================================================ package volcenginecertcenter import ( "context" "errors" "fmt" "log/slog" vecs "github.com/volcengine/volcengine-go-sdk/service/certificateservice" ve "github.com/volcengine/volcengine-go-sdk/volcengine" vesession "github.com/volcengine/volcengine-go-sdk/volcengine/session" "github.com/certimate-go/certimate/pkg/core/certmgr" "github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-certcenter/internal" ) type CertmgrConfig struct { // 火山引擎 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 火山引擎 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 火山引擎地域。 Region string `json:"region"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *internal.CertificateserviceClient } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 上传证书 // REF: https://www.volcengine.com/docs/6638/1365580 importCertificateReq := &vecs.ImportCertificateInput{ CertificateInfo: &vecs.CertificateInfoForImportCertificateInput{ CertificateChain: ve.String(certPEM), PrivateKey: ve.String(privkeyPEM), }, Repeatable: ve.Bool(false), } importCertificateResp, err := c.sdkClient.ImportCertificate(importCertificateReq) c.logger.Debug("sdk request 'certcenter.ImportCertificate'", slog.Any("request", importCertificateReq), slog.Any("response", importCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'certcenter.ImportCertificate': %w", err) } var sslId string if importCertificateResp.InstanceId != nil && *importCertificateResp.InstanceId != "" { sslId = *importCertificateResp.InstanceId } if importCertificateResp.RepeatId != nil && *importCertificateResp.RepeatId != "" { sslId = *importCertificateResp.RepeatId } if sslId == "" { return nil, errors.New("received empty certificate id, both `InstanceId` and `RepeatId` are empty") } return &certmgr.UploadResult{ CertId: sslId, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } func createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.CertificateserviceClient, error) { if region == "" { region = "cn-beijing" // 证书中心默认区域:北京 } config := ve.NewConfig(). WithAkSk(accessKeyId, accessKeySecret). WithRegion(region) session, err := vesession.NewSession(config) if err != nil { return nil, err } client := internal.NewCertificateserviceClient(session) return client, nil } ================================================ FILE: pkg/core/certmgr/providers/volcengine-certcenter/volcengine_certcenter_test.go ================================================ package volcenginecertcenter_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-certcenter" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string ) func init() { argsPrefix := "VOLCENGINECERTCENTER_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") } /* Shell command to run this test: go test -v ./volcengine_certcenter_test.go -args \ --VOLCENGINECERTCENTER_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --VOLCENGINECERTCENTER_INPUTKEYPATH="/path/to/your-input-key.pem" \ --VOLCENGINECERTCENTER_ACCESSKEYID="your-access-key-id" \ --VOLCENGINECERTCENTER_ACCESSKEYSECRET="your-access-key-secret" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/certmgr/providers/volcengine-live/volcengine_live.go ================================================ package volcenginelive import ( "context" "errors" "fmt" "log/slog" "strings" "time" velive "github.com/volcengine/volc-sdk-golang/service/live/v20230101" ve "github.com/volcengine/volcengine-go-sdk/volcengine" "github.com/certimate-go/certimate/pkg/core/certmgr" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // 火山引擎 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 火山引擎 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *velive.Live } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client := velive.NewInstance() client.SetAccessKey(config.AccessKeyId) client.SetSecretKey(config.AccessKeySecret) return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 查询证书列表,避免重复上传 // REF: https://www.volcengine.com/docs/6469/1186278#%E6%9F%A5%E8%AF%A2%E8%AF%81%E4%B9%A6%E5%88%97%E8%A1%A8 listCertReq := &velive.ListCertV2Body{} listCertResp, err := c.sdkClient.ListCertV2(ctx, listCertReq) c.logger.Debug("sdk request 'live.ListCertV2'", slog.Any("request", listCertReq), slog.Any("response", listCertResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'live.ListCertV2': %w", err) } if listCertResp.Result.CertList != nil { for _, certItem := range listCertResp.Result.CertList { // 查询证书详细信息 // REF: https://www.volcengine.com/docs/6469/1186278#%E6%9F%A5%E7%9C%8B%E8%AF%81%E4%B9%A6%E8%AF%A6%E6%83%85 describeCertDetailSecretReq := &velive.DescribeCertDetailSecretV2Body{ ChainID: ve.String(certItem.ChainID), } describeCertDetailSecretResp, err := c.sdkClient.DescribeCertDetailSecretV2(ctx, describeCertDetailSecretReq) c.logger.Debug("sdk request 'live.DescribeCertDetailSecretV2'", slog.Any("request", describeCertDetailSecretReq), slog.Any("response", describeCertDetailSecretResp)) if err != nil { continue } // 如果已存在相同证书,直接返回 oldCertPEM := strings.Join(describeCertDetailSecretResp.Result.SSL.Chain, "\n\n") if xcert.EqualCertificatesFromPEM(certPEM, oldCertPEM) { c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: certItem.ChainID, CertName: certItem.CertName, }, nil } } } // 生成新证书名(需符合火山引擎命名规则) certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) // 上传新证书 // REF: https://www.volcengine.com/docs/6469/1186278#%E6%B7%BB%E5%8A%A0%E8%AF%81%E4%B9%A6 createCertReq := &velive.CreateCertBody{ CertName: ve.String(certName), Rsa: velive.CreateCertBodyRsa{ Prikey: privkeyPEM, Pubkey: certPEM, }, UseWay: "https", } createCertResp, err := c.sdkClient.CreateCert(ctx, createCertReq) c.logger.Debug("sdk request 'live.CreateCert'", slog.Any("request", createCertReq), slog.Any("response", createCertResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'live.CreateCert': %w", err) } return &certmgr.UploadResult{ CertId: *createCertResp.Result.ChainID, CertName: certName, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { return nil, certmgr.ErrUnsupported } ================================================ FILE: pkg/core/certmgr/providers/wangsu-certificate/wangsu_certificate.go ================================================ package wangsucertificate import ( "context" "errors" "fmt" "log/slog" "regexp" "strings" "time" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" wangsusdk "github.com/certimate-go/certimate/pkg/sdk3rd/wangsu/certificate" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type CertmgrConfig struct { // 网宿云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 网宿云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` } type Certmgr struct { config *CertmgrConfig logger *slog.Logger sdkClient *wangsusdk.Client } var _ certmgr.Provider = (*Certmgr)(nil) func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { if config == nil { return nil, errors.New("the configuration of the certmgr provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Certmgr{ config: config, logger: slog.Default(), sdkClient: client, }, nil } func (c *Certmgr) SetLogger(logger *slog.Logger) { if logger == nil { c.logger = slog.New(slog.DiscardHandler) } else { c.logger = logger } } func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*certmgr.UploadResult, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } // 查询证书列表,避免重复上传 // REF: https://www.wangsu.com/document/api-doc/22675?productCode=certificatemanagement listCertificatesResp, err := c.sdkClient.ListCertificatesWithContext(ctx) c.logger.Debug("sdk request 'certificatemanagement.ListCertificates'", slog.Any("response", listCertificatesResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'certificatemanagement.ListCertificates': %w", err) } if listCertificatesResp.Certificates != nil { for _, certItem := range listCertificatesResp.Certificates { // 对比证书序列号 if !strings.EqualFold(certX509.SerialNumber.Text(16), certItem.Serial) { continue } // 对比证书有效期 timezoneOfCST := time.FixedZone("CST", 8*60*60) oldCertNotBefore, _ := time.ParseInLocation(time.DateTime, certItem.ValidityFrom, timezoneOfCST) oldCertNotAfter, _ := time.ParseInLocation(time.DateTime, certItem.ValidityTo, timezoneOfCST) if !certX509.NotBefore.Equal(oldCertNotBefore) || !certX509.NotAfter.Equal(oldCertNotAfter) { continue } // 如果以上信息都一致,则视为已存在相同证书,直接返回 c.logger.Info("ssl certificate already exists") return &certmgr.UploadResult{ CertId: certItem.CertificateId, CertName: certItem.Name, }, nil } } // 生成新证书名(需符合网宿云命名规则) certName := fmt.Sprintf("certimate_%d", time.Now().UnixMilli()) // 新增证书 // REF: https://www.wangsu.com/document/api-doc/25199?productCode=certificatemanagement createCertificateReq := &wangsusdk.CreateCertificateRequest{ Name: lo.ToPtr(certName), Certificate: lo.ToPtr(certPEM), PrivateKey: lo.ToPtr(privkeyPEM), Comment: lo.ToPtr("upload from certimate"), } createCertificateResp, err := c.sdkClient.CreateCertificateWithContext(ctx, createCertificateReq) c.logger.Debug("sdk request 'certificatemanagement.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'certificatemanagement.CreateCertificate': %w", err) } // 网宿云证书 URL 中包含证书 ID // 格式: // https://open.chinanetcenter.com/api/certificate/100001 wangsuCertIdMatches := regexp.MustCompile(`/certificate/([0-9]+)`).FindStringSubmatch(createCertificateResp.CertificateLocation) if len(wangsuCertIdMatches) == 0 { return nil, fmt.Errorf("received empty certificate id") } return &certmgr.UploadResult{ CertId: wangsuCertIdMatches[1], CertName: certName, }, nil } func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { certId := certIdOrName certName := fmt.Sprintf("certimate_%d", time.Now().UnixMilli()) // 修改证书 // REF: https://www.wangsu.com/document/api-doc/25568?productCode=certificatemanagement updateCertificateReq := &wangsusdk.UpdateCertificateRequest{ Name: lo.ToPtr(certName), Certificate: lo.ToPtr(certPEM), PrivateKey: lo.ToPtr(privkeyPEM), Comment: lo.ToPtr("upload from certimate"), } updateCertificateResp, err := c.sdkClient.UpdateCertificateWithContext(ctx, certId, updateCertificateReq) c.logger.Debug("sdk request 'certificatemanagement.UpdateCertificate'", slog.Any("request", updateCertificateReq), slog.Any("response", updateCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'certificatemanagement.UpdateCertificate': %w", err) } return &certmgr.OperateResult{}, nil } func createSDKClient(accessKeyId, accessKeySecret string) (*wangsusdk.Client, error) { return wangsusdk.NewClient(accessKeyId, accessKeySecret) } ================================================ FILE: pkg/core/certmgr/providers/wangsu-certificate/wangsu_certificate_test.go ================================================ package wangsucertificate_test import ( "context" "encoding/json" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/certmgr/providers/wangsu-certificate" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string ) func init() { argsPrefix := "WANGSUCERTIFICATE_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") } /* Shell command to run this test: go test -v ./wangsu_certificate_test.go -args \ --WANGSUCERTIFICATE_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --WANGSUCERTIFICATE_INPUTKEYPATH="/path/to/your-input-key.pem" \ --WANGSUCERTIFICATE_ACCESSKEYID="your-access-key-id" \ --WANGSUCERTIFICATE_ACCESSKEYSECRET="your-access-key-secret" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), }, "\n")) provider, err := provider.NewCertmgr(&provider.CertmgrConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } sres, _ := json.Marshal(res) t.Logf("ok: %s", string(sres)) }) } ================================================ FILE: pkg/core/deployer/provider.go ================================================ package deployer import ( "context" "log/slog" ) // 表示定义 SSL 证书部署器的抽象类型接口。 type Provider interface { // 设置日志记录器。 // // 入参: // - logger:日志记录器实例。 SetLogger(logger *slog.Logger) // 部署证书。 // // 入参: // - ctx:上下文。 // - certPEM:证书 PEM 内容。 // - privkeyPEM:私钥 PEM 内容。 // // 出参: // - res:部署结果。 // - err: 错误。 Deploy(ctx context.Context, certPEM, privkeyPEM string) (_res *DeployResult, _err error) } // 表示 SSL 证书部署结果的数据结构。 type DeployResult struct { ExtendedData map[string]any `json:"extendedData,omitempty"` } ================================================ FILE: pkg/core/deployer/providers/1panel/1panel.go ================================================ package onepanel import ( "context" "crypto/tls" "errors" "fmt" "log/slog" "strconv" "time" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/1panel" "github.com/certimate-go/certimate/pkg/core/deployer" onepanelsdk "github.com/certimate-go/certimate/pkg/sdk3rd/1panel" onepanelsdk2 "github.com/certimate-go/certimate/pkg/sdk3rd/1panel/v2" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xwait "github.com/certimate-go/certimate/pkg/utils/wait" ) type DeployerConfig struct { // 1Panel 服务地址。 ServerUrl string `json:"serverUrl"` // 1Panel 版本。 // 可取值 "v1"、"v2"。 ApiVersion string `json:"apiVersion"` // 1Panel 接口密钥。 ApiKey string `json:"apiKey"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` // 子节点名称。 // 选填。 NodeName string `json:"nodeName,omitempty"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 域名匹配模式。 // 零值时默认值 [WEBSITE_MATCH_PATTERN_SPECIFIED]。 WebsiteMatchPattern string `json:"websiteMatchPattern,omitempty"` // 网站 ID。 // 部署资源类型为 [RESOURCE_TYPE_WEBSITE]、且匹配模式非 [WEBSITE_MATCH_PATTERN_CERTSAN] 时必填。 WebsiteId int64 `json:"websiteId,omitempty"` // 证书 ID。 // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。 CertificateId int64 `json:"certificateId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient any sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ServerUrl, config.ApiVersion, config.ApiKey, config.AllowInsecureConnections, config.NodeName) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ ServerUrl: config.ServerUrl, ApiVersion: config.ApiVersion, ApiKey: config.ApiKey, AllowInsecureConnections: config.AllowInsecureConnections, NodeName: config.NodeName, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_WEBSITE: if err := d.deployToWebsite(ctx, certPEM, privkeyPEM); err != nil { return nil, err } case RESOURCE_TYPE_CERTIFICATE: if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToWebsite(ctx context.Context, certPEM, privkeyPEM string) error { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的网站列表 var websiteIds []int64 switch d.config.WebsiteMatchPattern { case "", WEBSITE_MATCH_PATTERN_SPECIFIED: { if d.config.WebsiteId == 0 { return errors.New("config `websiteId` is required") } websiteIds = []int64{d.config.WebsiteId} } case WEBSITE_MATCH_PATTERN_CERTSAN: { websiteIdCandidates, err := d.getMatchedWebsiteIdsByCertificate(ctx, certPEM) if err != nil { return err } websiteIds = websiteIdCandidates } default: return fmt.Errorf("unsupported website match pattern: '%s'", d.config.WebsiteMatchPattern) } // 遍历更新网站证书 if len(websiteIds) == 0 { d.logger.Info("no websites to deploy") } else { d.logger.Info("found websites to deploy", slog.Any("websiteIds", websiteIds)) var errs []error websiteSSLId, _ := strconv.ParseInt(upres.CertId, 10, 64) for i, websiteId := range websiteIds { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateWebsiteCertificate(ctx, websiteId, websiteSSLId); err != nil { errs = append(errs, err) } if i < len(websiteIds)-1 { xwait.DelayWithContext(ctx, time.Second*5) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.CertificateId == 0 { return errors.New("config `certificateId` is required") } // 替换证书 opres, err := d.sdkCertmgr.Replace(ctx, strconv.FormatInt(d.config.CertificateId, 10), certPEM, privkeyPEM) if err != nil { return fmt.Errorf("failed to replace certificate file: %w", err) } else { d.logger.Info("ssl certificate replaced", slog.Any("result", opres)) } return nil } func (d *Deployer) getMatchedWebsiteIdsByCertificate(ctx context.Context, certPEM string) ([]int64, error) { var websiteIds []int64 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } switch sdkClient := d.sdkClient.(type) { case *onepanelsdk.Client: { websiteSearchPage := 1 websiteSearchPageSize := 100 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } websiteSearchReq := &onepanelsdk.WebsiteSearchRequest{ Order: "ascending", OrderBy: "primary_domain", Page: int32(websiteSearchPage), PageSize: int32(websiteSearchPageSize), } websiteSearchResp, err := sdkClient.WebsiteSearchWithContext(ctx, websiteSearchReq) d.logger.Debug("sdk request '1panel.WebsiteSearch'", slog.Any("request", websiteSearchReq), slog.Any("response", websiteSearchResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request '1panel.WebsiteSearch': %w", err) } if websiteSearchResp.Data == nil { break } for _, websiteItem := range websiteSearchResp.Data.Items { if certX509.VerifyHostname(websiteItem.PrimaryDomain) != nil { continue } websiteGetResp, err := sdkClient.WebsiteGetWithContext(ctx, websiteItem.ID) d.logger.Debug("sdk request '1panel.WebsiteGet'", slog.Int64("websiteId", websiteItem.ID), slog.Any("response", websiteGetResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request '1panel.WebsiteGet': %w", err) } for _, domainInfo := range websiteGetResp.Data.Domains { if domainInfo.SSL || certX509.VerifyHostname(domainInfo.Domain) == nil { websiteIds = append(websiteIds, websiteItem.ID) break } } } if len(websiteSearchResp.Data.Items) < websiteSearchPageSize { break } websiteSearchPage++ } } case *onepanelsdk2.Client: { websiteSearchPage := 1 websiteSearchPageSize := 100 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } websiteSearchReq := &onepanelsdk2.WebsiteSearchRequest{ Order: "ascending", OrderBy: "primary_domain", Page: int32(websiteSearchPage), PageSize: int32(websiteSearchPageSize), } websiteSearchResp, err := sdkClient.WebsiteSearchWithContext(ctx, websiteSearchReq) d.logger.Debug("sdk request '1panel.WebsiteSearch'", slog.Any("request", websiteSearchReq), slog.Any("response", websiteSearchResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request '1panel.WebsiteSearch': %w", err) } if websiteSearchResp.Data == nil { break } for _, websiteItem := range websiteSearchResp.Data.Items { if certX509.VerifyHostname(websiteItem.PrimaryDomain) != nil { continue } websiteGetResp, err := sdkClient.WebsiteGetWithContext(ctx, websiteItem.ID) d.logger.Debug("sdk request '1panel.WebsiteGet'", slog.Int64("websiteId", websiteItem.ID), slog.Any("response", websiteGetResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request '1panel.WebsiteGet': %w", err) } for _, domainInfo := range websiteGetResp.Data.Domains { if domainInfo.SSL || certX509.VerifyHostname(domainInfo.Domain) == nil { websiteIds = append(websiteIds, websiteItem.ID) break } } } if len(websiteSearchResp.Data.Items) < websiteSearchPageSize { break } websiteSearchPage++ } } default: panic("unreachable") } if len(websiteIds) == 0 { return nil, errors.New("could not find any websites matched by certificate") } return websiteIds, nil } func (d *Deployer) updateWebsiteCertificate(ctx context.Context, websiteId int64, websiteSSLId int64) error { switch sdkClient := d.sdkClient.(type) { case *onepanelsdk.Client: { // 获取网站 HTTPS 配置 websiteHttpsGetResp, err := sdkClient.WebsiteHttpsGetWithContext(ctx, websiteId) d.logger.Debug("sdk request '1panel.WebsiteHttpsGet'", slog.Int64("websiteId", websiteId), slog.Any("response", websiteHttpsGetResp)) if err != nil { return fmt.Errorf("failed to execute sdk request '1panel.WebsiteHttpsGet': %w", err) } else { if websiteHttpsGetResp.Data.Enable && websiteHttpsGetResp.Data.WebsiteSSLID == websiteSSLId { return nil } } // 修改网站 HTTPS 配置 websiteHttpsPostReq := &onepanelsdk.WebsiteHttpsPostRequest{ WebsiteID: websiteId, Type: "existed", WebsiteSSLID: websiteSSLId, Enable: true, HttpConfig: websiteHttpsGetResp.Data.HttpConfig, SSLProtocol: websiteHttpsGetResp.Data.SSLProtocol, Algorithm: websiteHttpsGetResp.Data.Algorithm, Hsts: websiteHttpsGetResp.Data.Hsts, } if websiteHttpsPostReq.HttpConfig == "" { websiteHttpsPostReq.HttpConfig = "HTTPToHTTPS" } websiteHttpsPostResp, err := sdkClient.WebsiteHttpsPostWithContext(ctx, websiteId, websiteHttpsPostReq) d.logger.Debug("sdk request '1panel.WebsiteHttpsPost'", slog.Int64("websiteId", websiteId), slog.Any("request", websiteHttpsPostReq), slog.Any("response", websiteHttpsPostResp)) if err != nil { return fmt.Errorf("failed to execute sdk request '1panel.WebsiteHttpsPost': %w", err) } } case *onepanelsdk2.Client: { // 获取网站 HTTPS 配置 websiteHttpsGetResp, err := sdkClient.WebsiteHttpsGetWithContext(ctx, websiteId) d.logger.Debug("sdk request '1panel.WebsiteHttpsGet'", slog.Int64("websiteId", websiteId), slog.Any("response", websiteHttpsGetResp)) if err != nil { return fmt.Errorf("failed to execute sdk request '1panel.WebsiteHttpsGet': %w", err) } else { if websiteHttpsGetResp.Data.Enable && websiteHttpsGetResp.Data.WebsiteSSLID == websiteSSLId { return nil } } // 修改网站 HTTPS 配置 websiteHttpsPostReq := &onepanelsdk2.WebsiteHttpsPostRequest{ WebsiteID: websiteId, Type: "existed", WebsiteSSLID: websiteSSLId, Enable: true, HttpConfig: websiteHttpsGetResp.Data.HttpConfig, SSLProtocol: websiteHttpsGetResp.Data.SSLProtocol, Algorithm: websiteHttpsGetResp.Data.Algorithm, Hsts: websiteHttpsGetResp.Data.Hsts, Http3: websiteHttpsGetResp.Data.Http3, } if websiteHttpsPostReq.HttpConfig == "" { websiteHttpsPostReq.HttpConfig = "HTTPToHTTPS" } websiteHttpsPostResp, err := sdkClient.WebsiteHttpsPostWithContext(ctx, websiteId, websiteHttpsPostReq) d.logger.Debug("sdk request '1panel.WebsiteHttpsPost'", slog.Int64("websiteId", websiteId), slog.Any("request", websiteHttpsPostReq), slog.Any("response", websiteHttpsPostResp)) if err != nil { return fmt.Errorf("failed to execute sdk request '1panel.WebsiteHttpsPost': %w", err) } } default: panic("unreachable") } return nil } const ( sdkVersionV1 = "v1" sdkVersionV2 = "v2" ) func createSDKClient(serverUrl, apiVersion, apiKey string, skipTlsVerify bool, nodeName string) (any, error) { if apiVersion == sdkVersionV1 { client, err := onepanelsdk.NewClient(serverUrl, apiKey) if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } else if apiVersion == sdkVersionV2 { var client *onepanelsdk2.Client var err error if nodeName == "" { client, err = onepanelsdk2.NewClient(serverUrl, apiKey) } else { client, err = onepanelsdk2.NewClientWithNode(serverUrl, apiKey, nodeName) } if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } return nil, errors.New("1panel: invalid api version") } ================================================ FILE: pkg/core/deployer/providers/1panel/1panel_test.go ================================================ package onepanel_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/1panel" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fApiVersion string fApiKey string fWebsiteId int64 ) func init() { argsPrefix := "1PANEL_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fApiVersion, argsPrefix+"APIVERSION", "v1", "") flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") flag.Int64Var(&fWebsiteId, argsPrefix+"WEBSITEID", 0, "") } /* Shell command to run this test: go test -v ./1panel_test.go -args \ --1PANEL_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --1PANEL_INPUTKEYPATH="/path/to/your-input-key.pem" \ --1PANEL_SERVERURL="http://127.0.0.1:20410" \ --1PANEL_APIVERSION="v1" \ --1PANEL_APIKEY="your-api-key" \ --1PANEL_WEBSITEID="your-website-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("APIVERSION: %v", fApiVersion), fmt.Sprintf("APIKEY: %v", fApiKey), fmt.Sprintf("WEBSITEID: %v", fWebsiteId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ServerUrl: fServerUrl, ApiVersion: fApiVersion, ApiKey: fApiKey, AllowInsecureConnections: true, ResourceType: provider.RESOURCE_TYPE_WEBSITE, WebsiteMatchPattern: provider.WEBSITE_MATCH_PATTERN_SPECIFIED, WebsiteId: fWebsiteId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/1panel/consts.go ================================================ package onepanel const ( // 资源类型:替换指定网站的证书。 RESOURCE_TYPE_WEBSITE = "website" // 资源类型:替换指定证书。 RESOURCE_TYPE_CERTIFICATE = "certificate" ) const ( // 匹配模式:指定 ID。 WEBSITE_MATCH_PATTERN_SPECIFIED = "specified" // 匹配模式:证书 SAN 匹配。 WEBSITE_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/1panel-console/1panel_console.go ================================================ package onepanelconsole import ( "context" "crypto/tls" "errors" "fmt" "log/slog" "strconv" "github.com/certimate-go/certimate/pkg/core/deployer" onepanelsdk "github.com/certimate-go/certimate/pkg/sdk3rd/1panel" onepanelsdk2 "github.com/certimate-go/certimate/pkg/sdk3rd/1panel/v2" ) type DeployerConfig struct { // 1Panel 服务地址。 ServerUrl string `json:"serverUrl"` // 1Panel 版本。 // 可取值 "v1"、"v2"。 ApiVersion string `json:"apiVersion"` // 1Panel 接口密钥。 ApiKey string `json:"apiKey"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` // 是否自动重启。 AutoRestart bool `json:"autoRestart"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient any } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ServerUrl, config.ApiVersion, config.ApiKey, config.AllowInsecureConnections) 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) (*deployer.DeployResult, error) { // 设置面板 SSL 证书 switch sdkClient := d.sdkClient.(type) { case *onepanelsdk.Client: { settingsSSLUpdateReq := &onepanelsdk.SettingsSSLUpdateRequest{ Cert: certPEM, Key: privkeyPEM, SSL: "enable", SSLType: "import-paste", AutoRestart: strconv.FormatBool(d.config.AutoRestart), } settingsSSLUpdateResp, err := sdkClient.SettingsSSLUpdateWithContext(ctx, settingsSSLUpdateReq) d.logger.Debug("sdk request '1panel.SettingsSSLUpdate'", slog.Any("request", settingsSSLUpdateReq), slog.Any("response", settingsSSLUpdateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request '1panel.SettingsSSLUpdate': %w", err) } } case *onepanelsdk2.Client: { coreSettingsSSLUpdateReq := &onepanelsdk2.CoreSettingsSSLUpdateRequest{ Cert: certPEM, Key: privkeyPEM, SSL: "Enable", SSLType: "import-paste", AutoRestart: strconv.FormatBool(d.config.AutoRestart), } coreSettingsSSLUpdateResp, err := sdkClient.CoreSettingsSSLUpdateWithContext(ctx, coreSettingsSSLUpdateReq) d.logger.Debug("sdk request '1panel.CoreSettingsSSLUpdate'", slog.Any("request", coreSettingsSSLUpdateReq), slog.Any("response", coreSettingsSSLUpdateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request '1panel.CoreSettingsSSLUpdate': %w", err) } } default: panic("unreachable") } return &deployer.DeployResult{}, nil } const ( sdkVersionV1 = "v1" sdkVersionV2 = "v2" ) func createSDKClient(serverUrl, apiVersion, apiKey string, skipTlsVerify bool) (any, error) { if apiVersion == sdkVersionV1 { client, err := onepanelsdk.NewClient(serverUrl, apiKey) if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } else if apiVersion == sdkVersionV2 { client, err := onepanelsdk2.NewClient(serverUrl, apiKey) if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } return nil, errors.New("1panel: invalid api version") } ================================================ FILE: pkg/core/deployer/providers/1panel-console/1panel_console_test.go ================================================ package onepanelconsole_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/1panel-console" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fApiVersion string fApiKey string ) func init() { argsPrefix := "1PANELCONSOLE_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fApiVersion, argsPrefix+"APIVERSION", "v1", "") flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") } /* Shell command to run this test: go test -v ./1panel_console_test.go -args \ --1PANELCONSOLE_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --1PANELCONSOLE_INPUTKEYPATH="/path/to/your-input-key.pem" \ --1PANELCONSOLE_SERVERURL="http://127.0.0.1:20410" \ --1PANELCONSOLE_APIVERSION="v1" \ --1PANELCONSOLE_APIKEY="your-api-key" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("APIVERSION: %v", fApiVersion), fmt.Sprintf("APIKEY: %v", fApiKey), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ServerUrl: fServerUrl, ApiVersion: fApiVersion, ApiKey: fApiKey, AllowInsecureConnections: true, AutoRestart: true, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go ================================================ package aliyunalb import ( "context" "errors" "fmt" "log/slog" "strconv" "strings" "time" alialb "github.com/alibabacloud-go/alb-20200616/v2/client" alicas "github.com/alibabacloud-go/cas-20200407/v4/client" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" "github.com/alibabacloud-go/tea/dara" "github.com/alibabacloud-go/tea/tea" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-alb/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xwait "github.com/certimate-go/certimate/pkg/utils/wait" ) type DeployerConfig struct { // 阿里云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 阿里云资源组 ID。 ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 负载均衡实例 ID。 // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER] 时必填。 LoadbalancerId string `json:"loadbalancerId,omitempty"` // 负载均衡监听 ID。 // 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。 ListenerId string `json:"listenerId,omitempty"` // SNI 域名(支持泛域名)。 // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时选填。 Domain string `json:"domain,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClients *wSDKClients sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) type wSDKClients struct { ALB *internal.AlbClient CAS *internal.CasClient } func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } clients, err := createSDKClients(config.AccessKeyId, config.AccessKeySecret, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, ResourceGroupId: config.ResourceGroupId, Region: lo. If(config.Region == "" || strings.HasPrefix(config.Region, "cn-"), "cn-hangzhou"). Else("ap-southeast-1"), }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClients: clients, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_LOADBALANCER: if err := d.deployToLoadbalancer(ctx, upres.ExtendedData["CertIdentifier"].(string), certX509.DNSNames); err != nil { return nil, err } case RESOURCE_TYPE_LISTENER: if err := d.deployToListener(ctx, upres.ExtendedData["CertIdentifier"].(string), certX509.DNSNames); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string, cloudCertSANs []string) error { if d.config.LoadbalancerId == "" { return errors.New("config `loadbalancerId` is required") } // 查询负载均衡实例的详细信息 // REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getloadbalancerattribute getLoadBalancerAttributeReq := &alialb.GetLoadBalancerAttributeRequest{ LoadBalancerId: tea.String(d.config.LoadbalancerId), } getLoadBalancerAttributeResp, err := d.sdkClients.ALB.GetLoadBalancerAttributeWithContext(ctx, getLoadBalancerAttributeReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'alb.GetLoadBalancerAttribute'", slog.Any("request", getLoadBalancerAttributeReq), slog.Any("response", getLoadBalancerAttributeResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'alb.GetLoadBalancerAttribute': %w", err) } // 查询 HTTPS 监听列表 // REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners listenerIds := make([]string, 0) listListenersToken := (*string)(nil) for { select { case <-ctx.Done(): return ctx.Err() default: } listListenersReq := &alialb.ListListenersRequest{ NextToken: listListenersToken, MaxResults: tea.Int32(100), LoadBalancerIds: tea.StringSlice([]string{d.config.LoadbalancerId}), ListenerProtocol: tea.String("HTTPS"), } listListenersResp, err := d.sdkClients.ALB.ListListenersWithContext(ctx, listListenersReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'alb.ListListeners'", slog.Any("request", listListenersReq), slog.Any("response", listListenersResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'alb.ListListeners': %w", err) } if listListenersResp.Body == nil { break } for _, listener := range listListenersResp.Body.Listeners { listenerIds = append(listenerIds, tea.StringValue(listener.ListenerId)) } if len(listListenersResp.Body.Listeners) == 0 || listListenersResp.Body.NextToken == nil { break } listListenersToken = listListenersResp.Body.NextToken } // 查询 QUIC 监听列表 // REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners listListenersToken = nil for { select { case <-ctx.Done(): return ctx.Err() default: } listListenersReq := &alialb.ListListenersRequest{ NextToken: listListenersToken, MaxResults: tea.Int32(100), LoadBalancerIds: tea.StringSlice([]string{d.config.LoadbalancerId}), ListenerProtocol: tea.String("QUIC"), } listListenersResp, err := d.sdkClients.ALB.ListListenersWithContext(ctx, listListenersReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'alb.ListListeners'", slog.Any("request", listListenersReq), slog.Any("response", listListenersResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'alb.ListListeners': %w", err) } if listListenersResp.Body == nil { break } for _, listener := range listListenersResp.Body.Listeners { listenerIds = append(listenerIds, tea.StringValue(listener.ListenerId)) } if len(listListenersResp.Body.Listeners) == 0 || listListenersResp.Body.NextToken == nil { break } listListenersToken = listListenersResp.Body.NextToken } // 遍历更新监听证书 if len(listenerIds) == 0 { d.logger.Info("no alb listeners to deploy") } else { var errs []error d.logger.Info("found https/quic listeners to deploy", slog.Any("listenerIds", listenerIds)) for _, listenerId := range listenerIds { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId, cloudCertSANs); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) deployToListener(ctx context.Context, cloudCertId string, cloudCertSANs []string) error { if d.config.ListenerId == "" { return errors.New("config `listenerId` is required") } // 更新监听 if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId, cloudCertSANs); err != nil { return err } return nil } func (d *Deployer) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string, cloudCertSANs []string) error { if d.config.Domain == "" { // 未指定 SNI,只需部署到监听器 if err := d.waitForListenerReady(ctx, cloudListenerId); err != nil { return err } // 修改监听的属性 // REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-updatelistenerattribute updateListenerAttributeReq := &alialb.UpdateListenerAttributeRequest{ ListenerId: tea.String(cloudListenerId), Certificates: []*alialb.UpdateListenerAttributeRequestCertificates{{ CertificateId: tea.String(cloudCertId), }}, } updateListenerAttributeResp, err := d.sdkClients.ALB.UpdateListenerAttributeWithContext(ctx, updateListenerAttributeReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'alb.UpdateListenerAttribute'", slog.Any("request", updateListenerAttributeReq), slog.Any("response", updateListenerAttributeResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'alb.UpdateListenerAttribute': %w", err) } } else { // 指定 SNI,需部署到扩展域名 // 查询监听证书列表 // REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlistenercertificates listenerCertificates := make([]alialb.ListListenerCertificatesResponseBodyCertificates, 0) listListenerCertificatesToken := (*string)(nil) for { select { case <-ctx.Done(): return ctx.Err() default: } listListenerCertificatesReq := &alialb.ListListenerCertificatesRequest{ NextToken: listListenerCertificatesToken, MaxResults: tea.Int32(100), ListenerId: tea.String(cloudListenerId), CertificateType: tea.String("Server"), } listListenerCertificatesResp, err := d.sdkClients.ALB.ListListenerCertificatesWithContext(ctx, listListenerCertificatesReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'alb.ListListenerCertificates'", slog.Any("request", listListenerCertificatesReq), slog.Any("response", listListenerCertificatesResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'alb.ListListenerCertificates': %w", err) } if listListenerCertificatesResp.Body == nil { break } for _, listenerCertificate := range listListenerCertificatesResp.Body.Certificates { listenerCertificates = append(listenerCertificates, *listenerCertificate) } if len(listListenerCertificatesResp.Body.Certificates) == 0 || listListenerCertificatesResp.Body.NextToken == nil { break } listListenerCertificatesToken = listListenerCertificatesResp.Body.NextToken } // 查询监听证书,并找出需要解除关联的证书 // REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlistenercertificates // REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-getcertificatedetail certificateIsAlreadyAssociated := false certificateIdsToDissociate := make([]string, 0) if len(listenerCertificates) > 0 { d.logger.Info("found listener certificates to deploy", slog.Any("listenerCertificates", listenerCertificates)) var errs []error for _, listenerCertificate := range listenerCertificates { if tea.BoolValue(listenerCertificate.IsDefault) { continue } if !strings.EqualFold(tea.StringValue(listenerCertificate.Status), "Associated") { continue } if tea.StringValue(listenerCertificate.CertificateId) == cloudCertId { certificateIsAlreadyAssociated = true break } certificateId := strings.SplitN(tea.StringValue(listenerCertificate.CertificateId), "-", 2)[0] certificateIdAsInt64, err := strconv.ParseInt(certificateId, 10, 64) if err != nil { errs = append(errs, err) continue } getCertificateDetailReq := &alicas.GetCertificateDetailRequest{ CertificateId: tea.Int64(certificateIdAsInt64), } getCertificateDetailResp, err := d.sdkClients.CAS.GetCertificateDetailWithContext(ctx, getCertificateDetailReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'cas.GetCertificateDetail'", slog.Any("request", getCertificateDetailReq), slog.Any("response", getCertificateDetailResp)) if err != nil { if sdkerr, ok := err.(*tea.SDKError); ok { if tea.IntValue(sdkerr.StatusCode) == 400 && tea.StringValue(sdkerr.Code) == "NotFound" { continue } } errs = append(errs, fmt.Errorf("failed to execute sdk request 'cas.GetCertificateDetail': %w", err)) continue } else { certCNMatched := tea.StringValue(getCertificateDetailResp.Body.CommonName) == d.config.Domain certSANDiff, _ := lo.Difference(tea.StringSliceValue(getCertificateDetailResp.Body.SubjectAlternativeNames), cloudCertSANs) if certCNMatched || len(certSANDiff) == 0 { certificateIdsToDissociate = append(certificateIdsToDissociate, certificateId) continue } certNotAfter := time.Unix(tea.Int64Value(getCertificateDetailResp.Body.NotAfter)/1000, 0) if certNotAfter.Before(time.Now()) { certificateIdsToDissociate = append(certificateIdsToDissociate, certificateId) continue } } } if len(errs) > 0 { return errors.Join(errs...) } } // 关联监听和扩展证书 // REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-associateadditionalcertificateswithlistener if !certificateIsAlreadyAssociated { if err := d.waitForListenerReady(ctx, cloudListenerId); err != nil { return err } associateAdditionalCertificatesFromListenerReq := &alialb.AssociateAdditionalCertificatesWithListenerRequest{ ListenerId: tea.String(cloudListenerId), Certificates: []*alialb.AssociateAdditionalCertificatesWithListenerRequestCertificates{ { CertificateId: tea.String(cloudCertId), }, }, } associateAdditionalCertificatesFromListenerResp, err := d.sdkClients.ALB.AssociateAdditionalCertificatesWithListenerWithContext(ctx, associateAdditionalCertificatesFromListenerReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'alb.AssociateAdditionalCertificatesWithListener'", slog.Any("request", associateAdditionalCertificatesFromListenerReq), slog.Any("response", associateAdditionalCertificatesFromListenerResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'alb.AssociateAdditionalCertificatesWithListener': %w", err) } } // 解除关联监听和扩展证书 // REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-dissociateadditionalcertificatesfromlistener if !certificateIsAlreadyAssociated && len(certificateIdsToDissociate) > 0 { if err := d.waitForListenerReady(ctx, cloudListenerId); err != nil { return err } dissociateAdditionalCertificatesFromListenerReq := &alialb.DissociateAdditionalCertificatesFromListenerRequest{ ListenerId: tea.String(cloudListenerId), Certificates: lo.Map(certificateIdsToDissociate, func(certificateId string, _ int) *alialb.DissociateAdditionalCertificatesFromListenerRequestCertificates { return &alialb.DissociateAdditionalCertificatesFromListenerRequestCertificates{ CertificateId: tea.String(certificateId), } }), } dissociateAdditionalCertificatesFromListenerResp, err := d.sdkClients.ALB.DissociateAdditionalCertificatesFromListenerWithContext(ctx, dissociateAdditionalCertificatesFromListenerReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'alb.DissociateAdditionalCertificatesFromListener'", slog.Any("request", dissociateAdditionalCertificatesFromListenerReq), slog.Any("response", dissociateAdditionalCertificatesFromListenerResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'alb.DissociateAdditionalCertificatesFromListener': %w", err) } } } return nil } func (d *Deployer) waitForListenerReady(ctx context.Context, cloudListenerId string) error { // 查询监听的属性,直到监听状态不再为 "Configuring" // REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getlistenerattribute if _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) { getListenerAttributeReq := &alialb.GetListenerAttributeRequest{ ListenerId: tea.String(cloudListenerId), } getListenerAttributeResp, err := d.sdkClients.ALB.GetListenerAttributeWithContext(ctx, getListenerAttributeReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'alb.GetListenerAttribute'", slog.Any("request", getListenerAttributeReq), slog.Any("response", getListenerAttributeResp)) if err != nil { return false, fmt.Errorf("failed to execute sdk request 'alb.GetListenerAttribute': %w", err) } if tea.StringValue(getListenerAttributeResp.Body.ListenerStatus) != "Configuring" { return true, nil } d.logger.Info("waiting for aliyun alb listener's status to not be 'Configuring' ...") return false, nil }, time.Second*5); err != nil { return err } return nil } func createSDKClients(accessKeyId, accessKeySecret, region string) (*wSDKClients, error) { // 接入点一览 https://api.aliyun.com/product/Alb var albEndpoint string switch region { case "", "cn-hangzhou-finance": albEndpoint = "alb.cn-hangzhou.aliyuncs.com" default: albEndpoint = fmt.Sprintf("alb.%s.aliyuncs.com", region) } albConfig := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), Endpoint: tea.String(albEndpoint), } albClient, err := internal.NewAlbClient(albConfig) if err != nil { return nil, err } // 接入点一览 https://api.aliyun.com/product/cas var casEndpoint string if !strings.HasPrefix(region, "cn-") { casEndpoint = "cas.ap-southeast-1.aliyuncs.com" } else { casEndpoint = "cas.aliyuncs.com" } casConfig := &aliopen.Config{ Endpoint: tea.String(casEndpoint), AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), } casClient, err := internal.NewCasClient(casConfig) if err != nil { return nil, err } return &wSDKClients{ ALB: albClient, CAS: casClient, }, nil } ================================================ FILE: pkg/core/deployer/providers/aliyun-alb/aliyun_alb_test.go ================================================ package aliyunalb_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-alb" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fRegion string fLoadbalancerId string fListenerId string fDomain string ) func init() { argsPrefix := "ALIYUNALB_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "") flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./aliyun_alb_test.go -args \ --ALIYUNALB_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --ALIYUNALB_INPUTKEYPATH="/path/to/your-input-key.pem" \ --ALIYUNALB_ACCESSKEYID="your-access-key-id" \ --ALIYUNALB_ACCESSKEYSECRET="your-access-key-secret" \ --ALIYUNALB_REGION="cn-hangzhou" \ --ALIYUNALB_LOADBALANCERID="your-alb-instance-id" \ --ALIYUNALB_LISTENERID="your-alb-listener-id" \ --ALIYUNALB_DOMAIN="your-alb-sni-domain" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy_ToLoadbalancer", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Region: fRegion, ResourceType: provider.RESOURCE_TYPE_LOADBALANCER, LoadbalancerId: fLoadbalancerId, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) t.Run("Deploy_ToListener", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("LISTENERID: %v", fListenerId), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Region: fRegion, ResourceType: provider.RESOURCE_TYPE_LISTENER, ListenerId: fListenerId, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/aliyun-alb/consts.go ================================================ package aliyunalb const ( // 资源类型:部署到指定负载均衡器。 RESOURCE_TYPE_LOADBALANCER = "loadbalancer" // 资源类型:部署到指定监听器。 RESOURCE_TYPE_LISTENER = "listener" ) ================================================ FILE: pkg/core/deployer/providers/aliyun-alb/internal/client.go ================================================ package internal import ( "context" alialb "github.com/alibabacloud-go/alb-20200616/v2/client" alicas "github.com/alibabacloud-go/cas-20200407/v4/client" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" openapiutil "github.com/alibabacloud-go/darabonba-openapi/v2/utils" "github.com/alibabacloud-go/tea/dara" ) // This is a partial copy of https://github.com/alibabacloud-go/cas-20200407/blob/master/client/client_context_func.go // to lightweight the vendor packages in the built binary. type CasClient struct { openapi.Client DisableSDKError *bool } func NewCasClient(config *openapiutil.Config) (*CasClient, error) { client := new(CasClient) err := client.Init(config) return client, err } func (client *CasClient) Init(config *openapiutil.Config) (_err error) { _err = client.Client.Init(config) if _err != nil { return _err } _err = client.CheckConfig(config) if _err != nil { return _err } return nil } func (client *CasClient) GetCertificateDetailWithContext(ctx context.Context, request *alicas.GetCertificateDetailRequest, runtime *dara.RuntimeOptions) (_result *alicas.GetCertificateDetailResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.CertificateId) { query["CertificateId"] = request.CertificateId } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("GetCertificateDetail"), Version: dara.String("2020-04-07"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alicas.GetCertificateDetailResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } // This is a partial copy of https://github.com/alibabacloud-go/alb-20200616/blob/master/client/client_context_func.go // to lightweight the vendor packages in the built binary. type AlbClient struct { openapi.Client DisableSDKError *bool } func NewAlbClient(config *openapiutil.Config) (*AlbClient, error) { client := new(AlbClient) err := client.Init(config) return client, err } func (client *AlbClient) Init(config *openapiutil.Config) (_err error) { _err = client.Client.Init(config) if _err != nil { return _err } _err = client.CheckConfig(config) if _err != nil { return _err } return nil } func (client *AlbClient) AssociateAdditionalCertificatesWithListenerWithContext(ctx context.Context, request *alialb.AssociateAdditionalCertificatesWithListenerRequest, runtime *dara.RuntimeOptions) (_result *alialb.AssociateAdditionalCertificatesWithListenerResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.Certificates) { query["Certificates"] = request.Certificates } if !dara.IsNil(request.ClientToken) { query["ClientToken"] = request.ClientToken } if !dara.IsNil(request.DryRun) { query["DryRun"] = request.DryRun } if !dara.IsNil(request.ListenerId) { query["ListenerId"] = request.ListenerId } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("AssociateAdditionalCertificatesWithListener"), Version: dara.String("2020-06-16"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alialb.AssociateAdditionalCertificatesWithListenerResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *AlbClient) DissociateAdditionalCertificatesFromListenerWithContext(ctx context.Context, request *alialb.DissociateAdditionalCertificatesFromListenerRequest, runtime *dara.RuntimeOptions) (_result *alialb.DissociateAdditionalCertificatesFromListenerResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.Certificates) { query["Certificates"] = request.Certificates } if !dara.IsNil(request.ClientToken) { query["ClientToken"] = request.ClientToken } if !dara.IsNil(request.DryRun) { query["DryRun"] = request.DryRun } if !dara.IsNil(request.ListenerId) { query["ListenerId"] = request.ListenerId } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("DissociateAdditionalCertificatesFromListener"), Version: dara.String("2020-06-16"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alialb.DissociateAdditionalCertificatesFromListenerResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *AlbClient) GetListenerAttributeWithContext(ctx context.Context, request *alialb.GetListenerAttributeRequest, runtime *dara.RuntimeOptions) (_result *alialb.GetListenerAttributeResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.ListenerId) { query["ListenerId"] = request.ListenerId } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("GetListenerAttribute"), Version: dara.String("2020-06-16"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alialb.GetListenerAttributeResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *AlbClient) GetLoadBalancerAttributeWithContext(ctx context.Context, request *alialb.GetLoadBalancerAttributeRequest, runtime *dara.RuntimeOptions) (_result *alialb.GetLoadBalancerAttributeResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.LoadBalancerId) { query["LoadBalancerId"] = request.LoadBalancerId } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("GetLoadBalancerAttribute"), Version: dara.String("2020-06-16"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alialb.GetLoadBalancerAttributeResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *AlbClient) ListListenerCertificatesWithContext(ctx context.Context, request *alialb.ListListenerCertificatesRequest, runtime *dara.RuntimeOptions) (_result *alialb.ListListenerCertificatesResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.CertificateIds) { query["CertificateIds"] = request.CertificateIds } if !dara.IsNil(request.CertificateType) { query["CertificateType"] = request.CertificateType } if !dara.IsNil(request.ListenerId) { query["ListenerId"] = request.ListenerId } if !dara.IsNil(request.MaxResults) { query["MaxResults"] = request.MaxResults } if !dara.IsNil(request.NextToken) { query["NextToken"] = request.NextToken } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("ListListenerCertificates"), Version: dara.String("2020-06-16"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alialb.ListListenerCertificatesResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *AlbClient) ListListenersWithContext(ctx context.Context, request *alialb.ListListenersRequest, runtime *dara.RuntimeOptions) (_result *alialb.ListListenersResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.ListenerIds) { query["ListenerIds"] = request.ListenerIds } if !dara.IsNil(request.ListenerProtocol) { query["ListenerProtocol"] = request.ListenerProtocol } if !dara.IsNil(request.LoadBalancerIds) { query["LoadBalancerIds"] = request.LoadBalancerIds } if !dara.IsNil(request.MaxResults) { query["MaxResults"] = request.MaxResults } if !dara.IsNil(request.NextToken) { query["NextToken"] = request.NextToken } if !dara.IsNil(request.Tag) { query["Tag"] = request.Tag } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("ListListeners"), Version: dara.String("2020-06-16"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alialb.ListListenersResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *AlbClient) UpdateListenerAttributeWithContext(ctx context.Context, request *alialb.UpdateListenerAttributeRequest, runtime *dara.RuntimeOptions) (_result *alialb.UpdateListenerAttributeResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.CaCertificates) { query["CaCertificates"] = request.CaCertificates } if !dara.IsNil(request.CaEnabled) { query["CaEnabled"] = request.CaEnabled } if !dara.IsNil(request.Certificates) { query["Certificates"] = request.Certificates } if !dara.IsNil(request.ClientToken) { query["ClientToken"] = request.ClientToken } if !dara.IsNil(request.DefaultActions) { query["DefaultActions"] = request.DefaultActions } if !dara.IsNil(request.DryRun) { query["DryRun"] = request.DryRun } if !dara.IsNil(request.GzipEnabled) { query["GzipEnabled"] = request.GzipEnabled } if !dara.IsNil(request.Http2Enabled) { query["Http2Enabled"] = request.Http2Enabled } if !dara.IsNil(request.IdleTimeout) { query["IdleTimeout"] = request.IdleTimeout } if !dara.IsNil(request.ListenerDescription) { query["ListenerDescription"] = request.ListenerDescription } if !dara.IsNil(request.ListenerId) { query["ListenerId"] = request.ListenerId } if !dara.IsNil(request.QuicConfig) { query["QuicConfig"] = request.QuicConfig } if !dara.IsNil(request.RequestTimeout) { query["RequestTimeout"] = request.RequestTimeout } if !dara.IsNil(request.SecurityPolicyId) { query["SecurityPolicyId"] = request.SecurityPolicyId } if !dara.IsNil(request.XForwardedForConfig) { query["XForwardedForConfig"] = request.XForwardedForConfig } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("UpdateListenerAttribute"), Version: dara.String("2020-06-16"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alialb.UpdateListenerAttributeResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } ================================================ FILE: pkg/core/deployer/providers/aliyun-apigw/aliyun_apigw.go ================================================ package aliyunapigw import ( "context" "errors" "fmt" "log/slog" "strings" "time" aliapig "github.com/alibabacloud-go/apig-20240327/v6/client" alicloudapi "github.com/alibabacloud-go/cloudapi-20160714/v5/client" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" "github.com/alibabacloud-go/tea/dara" "github.com/alibabacloud-go/tea/tea" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-apigw/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 阿里云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 阿里云资源组 ID。 ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 服务类型。 ServiceType string `json:"serviceType"` // API 网关 ID。 // 服务类型为 [SERVICE_TYPE_CLOUDNATIVE] 时必填。 GatewayId string `json:"gatewayId,omitempty"` // API 分组 ID。 // 服务类型为 [SERVICE_TYPE_TRADITIONAL] 时必填。 GroupId string `json:"groupId,omitempty"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 自定义域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClients *wSDKClients sdkCertmgr certmgr.Provider } type wSDKClients struct { CloudNativeAPIGateway *internal.ApigClient TraditionalAPIGateway *internal.CloudapiClient } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } clients, err := createSDKClients(config.AccessKeyId, config.AccessKeySecret, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, ResourceGroupId: config.ResourceGroupId, Region: lo. If(config.Region == "" || strings.HasPrefix(config.Region, "cn-"), "cn-hangzhou"). Else("ap-southeast-1"), }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClients: clients, sdkCertmgr: pcertmgr, }, 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) (*deployer.DeployResult, error) { switch d.config.ServiceType { case SERVICE_TYPE_TRADITIONAL: if err := d.deployToTraditional(ctx, certPEM, privkeyPEM); err != nil { return nil, err } case SERVICE_TYPE_CLOUDNATIVE: if err := d.deployToCloudNative(ctx, certPEM, privkeyPEM); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported service type '%s'", string(d.config.ServiceType)) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToTraditional(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.GroupId == "" { return errors.New("config `groupId` is required") } // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getTraditionalAllDomainsByGroupId(ctx, d.config.GroupId) if err != nil { return err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return xcerthostname.IsMatch(d.config.Domain, domain) }) if len(domains) == 0 { return errors.New("could not find any domains matched by wildcard") } } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return err } domainCandidates, err := d.getTraditionalAllDomainsByGroupId(ctx, d.config.GroupId) if err != nil { return err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return errors.New("could not find any domains matched by certificate") } } default: return fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no apigw domains to deploy") } else { d.logger.Info("found apigw domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateTraditionalDomainCertificate(ctx, d.config.GroupId, domain, certPEM, privkeyPEM); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) deployToCloudNative(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.GatewayId == "" { return errors.New("config `gatewayId` is required") } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getCloudNativeAllDomainsByGatewayId(ctx, d.config.GatewayId) if err != nil { return err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return xcerthostname.IsMatch(d.config.Domain, domain) }) if len(domains) == 0 { return errors.New("could not find any domains matched by wildcard") } } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return err } domainCandidates, err := d.getCloudNativeAllDomainsByGatewayId(ctx, d.config.GatewayId) if err != nil { return err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return errors.New("could not find any domains matched by certificate") } } default: return fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no apigw domains to deploy") } else { d.logger.Info("found apigw domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return ctx.Err() default: certId := upres.ExtendedData["CertIdentifier"].(string) if err := d.updateCloudNativeDomainCertificate(ctx, d.config.GatewayId, domain, certId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) getTraditionalAllDomainsByGroupId(ctx context.Context, cloudGroupId string) ([]string, error) { domains := make([]string, 0) // 查询 API 分组详情 // REF: https://help.aliyun.com/zh/api-gateway/traditional-api-gateway/developer-reference/api-cloudapi-2016-07-14-describeapigroup describeApiGroupReq := &alicloudapi.DescribeApiGroupRequest{ GroupId: tea.String(cloudGroupId), } describeApiGroupResp, err := d.sdkClients.TraditionalAPIGateway.DescribeApiGroupWithContext(ctx, describeApiGroupReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'apigateway.DescribeApiGroup'", slog.Any("request", describeApiGroupReq), slog.Any("response", describeApiGroupResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'apigateway.DescribeApiGroup': %w", err) } for _, domainItem := range describeApiGroupResp.Body.CustomDomains.DomainItem { if strings.EqualFold(tea.StringValue(domainItem.DomainBindingStatus), "BINDING") { domains = append(domains, tea.StringValue(domainItem.DomainName)) } } return domains, nil } func (d *Deployer) getCloudNativeAllDomainsByGatewayId(ctx context.Context, cloudGatewayId string) ([]string, error) { domains := make([]string, 0) // 查询域名列表 // REF: https://help.aliyun.com/zh/api-gateway/cloud-native-api-gateway/developer-reference/api-apig-2024-03-27-listdomains listDomainsPageNumber := 1 listDomainsPageSize := 10 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } listDomainsReq := &aliapig.ListDomainsRequest{ ResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId), GatewayId: tea.String(cloudGatewayId), PageNumber: tea.Int32(int32(listDomainsPageNumber)), PageSize: tea.Int32(int32(listDomainsPageSize)), } listDomainsResp, err := d.sdkClients.CloudNativeAPIGateway.ListDomainsWithContext(ctx, listDomainsReq, make(map[string]*string), &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'apig.ListDomains'", slog.Any("request", listDomainsReq), slog.Any("response", listDomainsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'apig.ListDomains': %w", err) } if listDomainsResp.Body == nil || listDomainsResp.Body.Data == nil { break } for _, domainItem := range listDomainsResp.Body.Data.Items { if strings.EqualFold(tea.StringValue(domainItem.Status), "Published") { domains = append(domains, tea.StringValue(domainItem.Name)) } } if len(listDomainsResp.Body.Data.Items) < listDomainsPageSize { break } listDomainsPageNumber++ } return domains, nil } func (d *Deployer) updateTraditionalDomainCertificate(ctx context.Context, cloudGroupId string, domain string, certPEM, privkeyPEM string) error { // 为自定义域名添加 SSL 证书 // REF: https://help.aliyun.com/zh/api-gateway/traditional-api-gateway/developer-reference/api-cloudapi-2016-07-14-setdomaincertificate setDomainCertificateReq := &alicloudapi.SetDomainCertificateRequest{ GroupId: tea.String(cloudGroupId), DomainName: tea.String(domain), CertificateName: tea.String(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())), CertificateBody: tea.String(certPEM), CertificatePrivateKey: tea.String(privkeyPEM), } setDomainCertificateResp, err := d.sdkClients.TraditionalAPIGateway.SetDomainCertificateWithContext(ctx, setDomainCertificateReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'apigateway.SetDomainCertificate'", slog.Any("request", setDomainCertificateReq), slog.Any("response", setDomainCertificateResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'apigateway.SetDomainCertificate': %w", err) } return nil } func (d *Deployer) updateCloudNativeDomainCertificate(ctx context.Context, cloudGatewayId string, domain string, cloudCertId string) error { // 获取域名 ID domainId, err := d.findCloudNativeDomainIdByDomain(ctx, cloudGatewayId, domain) if err != nil { return err } // 查询域名 // REF: https://help.aliyun.com/zh/api-gateway/cloud-native-api-gateway/developer-reference/api-apig-2024-03-27-getdomain getDomainReq := &aliapig.GetDomainRequest{} getDomainResp, err := d.sdkClients.CloudNativeAPIGateway.GetDomainWithContext(ctx, tea.String(domainId), getDomainReq, make(map[string]*string), &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'apig.GetDomain'", slog.String("domainId", domainId), slog.Any("request", getDomainReq), slog.Any("response", getDomainResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'apig.GetDomain': %w", err) } // 更新域名 // REF: https://help.aliyun.com/zh/api-gateway/cloud-native-api-gateway/developer-reference/api-apig-2024-03-27-updatedomain updateDomainReq := &aliapig.UpdateDomainRequest{ Protocol: tea.String("HTTPS"), ForceHttps: getDomainResp.Body.Data.ForceHttps, MTLSEnabled: getDomainResp.Body.Data.MTLSEnabled, Http2Option: getDomainResp.Body.Data.Http2Option, TlsMin: getDomainResp.Body.Data.TlsMin, TlsMax: getDomainResp.Body.Data.TlsMax, TlsCipherSuitesConfig: getDomainResp.Body.Data.TlsCipherSuitesConfig, CertIdentifier: tea.String(cloudCertId), } updateDomainResp, err := d.sdkClients.CloudNativeAPIGateway.UpdateDomainWithContext(ctx, tea.String(domainId), updateDomainReq, make(map[string]*string), &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'apig.UpdateDomain'", slog.String("domainId", domainId), slog.Any("request", updateDomainReq), slog.Any("response", updateDomainResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'apig.UpdateDomain': %w", err) } return nil } func (d *Deployer) findCloudNativeDomainIdByDomain(ctx context.Context, cloudGatewayId string, domain string) (string, error) { // 查询域名列表 // REF: https://help.aliyun.com/zh/api-gateway/cloud-native-api-gateway/developer-reference/api-apig-2024-03-27-listdomains listDomainsPageNumber := 1 listDomainsPageSize := 10 for { select { case <-ctx.Done(): return "", ctx.Err() default: } listDomainsReq := &aliapig.ListDomainsRequest{ ResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId), GatewayId: tea.String(cloudGatewayId), NameLike: tea.String(domain), PageNumber: tea.Int32(int32(listDomainsPageNumber)), PageSize: tea.Int32(int32(listDomainsPageSize)), } listDomainsResp, err := d.sdkClients.CloudNativeAPIGateway.ListDomainsWithContext(ctx, listDomainsReq, make(map[string]*string), &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'apig.ListDomains'", slog.Any("request", listDomainsReq), slog.Any("response", listDomainsResp)) if err != nil { return "", fmt.Errorf("failed to execute sdk request 'apig.ListDomains': %w", err) } if listDomainsResp.Body == nil || listDomainsResp.Body.Data == nil { break } for _, domainItem := range listDomainsResp.Body.Data.Items { if strings.EqualFold(tea.StringValue(domainItem.Name), domain) { return tea.StringValue(domainItem.DomainId), nil } } if len(listDomainsResp.Body.Data.Items) < listDomainsPageSize { break } listDomainsPageNumber++ } return "", fmt.Errorf("could not find domain '%s'", domain) } func createSDKClients(accessKeyId, accessKeySecret, region string) (*wSDKClients, error) { // 接入点一览 https://api.aliyun.com/product/APIG var cloudNativeAPIGEndpoint string switch region { case "": cloudNativeAPIGEndpoint = "apig.cn-hangzhou.aliyuncs.com" default: cloudNativeAPIGEndpoint = fmt.Sprintf("apig.%s.aliyuncs.com", region) } cloudNativeAPIGConfig := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), Endpoint: tea.String(cloudNativeAPIGEndpoint), } cloudNativeAPIGClient, err := internal.NewApigClient(cloudNativeAPIGConfig) if err != nil { return nil, err } // 接入点一览 https://api.aliyun.com/product/CloudAPI var traditionalAPIGEndpoint string switch region { case "": traditionalAPIGEndpoint = "apigateway.cn-hangzhou.aliyuncs.com" default: traditionalAPIGEndpoint = fmt.Sprintf("apigateway.%s.aliyuncs.com", region) } traditionalAPIGConfig := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), Endpoint: tea.String(traditionalAPIGEndpoint), } traditionalAPIGClient, err := internal.NewCloudapiClient(traditionalAPIGConfig) if err != nil { return nil, err } return &wSDKClients{ CloudNativeAPIGateway: cloudNativeAPIGClient, TraditionalAPIGateway: traditionalAPIGClient, }, nil } ================================================ FILE: pkg/core/deployer/providers/aliyun-apigw/aliyun_apigw_test.go ================================================ package aliyunapigw_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-apigw" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fRegion string fServiceType string fGatewayId string fGroupId string fDomain string ) func init() { argsPrefix := "ALIYUNAPIGW_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fGatewayId, argsPrefix+"GATEWARYID", "", "") flag.StringVar(&fGroupId, argsPrefix+"GROUPID", "", "") flag.StringVar(&fServiceType, argsPrefix+"SERVICETYPE", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./aliyun_apigw_test.go -args \ --ALIYUNAPIGW_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --ALIYUNAPIGW_INPUTKEYPATH="/path/to/your-input-key.pem" \ --ALIYUNAPIGW_ACCESSKEYID="your-access-key-id" \ --ALIYUNAPIGW_ACCESSKEYSECRET="your-access-key-secret" \ --ALIYUNAPIGW_REGION="cn-hangzhou" \ --ALIYUNAPIGW_SERVICETYPE="cloudnative" \ --ALIYUNAPIGW_GATEWAYID="your-api-gateway-id" \ --ALIYUNAPIGW_GROUPID="your-api-group-id" \ --ALIYUNAPIGW_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("SERVICETYPE: %v", fServiceType), fmt.Sprintf("GATEWAYID: %v", fGatewayId), fmt.Sprintf("GROUPID: %v", fGroupId), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Region: fRegion, ServiceType: fServiceType, GatewayId: fGatewayId, GroupId: fGroupId, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/aliyun-apigw/consts.go ================================================ package aliyunapigw const ( // 服务类型:原 API 网关。 SERVICE_TYPE_TRADITIONAL = "traditional" // 服务类型:云原生 API 网关。 SERVICE_TYPE_CLOUDNATIVE = "cloudnative" ) const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/aliyun-apigw/internal/client.go ================================================ package internal import ( "context" aliapig "github.com/alibabacloud-go/apig-20240327/v6/client" alicloudapi "github.com/alibabacloud-go/cloudapi-20160714/v5/client" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" openapiutil "github.com/alibabacloud-go/darabonba-openapi/v2/utils" "github.com/alibabacloud-go/tea/dara" ) // This is a partial copy of https://github.com/alibabacloud-go/apig-20240327/blob/master/client/client_context_func.go // to lightweight the vendor packages in the built binary. type ApigClient struct { openapi.Client DisableSDKError *bool } func NewApigClient(config *openapiutil.Config) (*ApigClient, error) { client := new(ApigClient) err := client.Init(config) return client, err } func (client *ApigClient) GetDomainWithContext(ctx context.Context, domainId *string, request *aliapig.GetDomainRequest, headers map[string]*string, runtime *dara.RuntimeOptions) (_result *aliapig.GetDomainResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.WithStatistics) { query["withStatistics"] = request.WithStatistics } req := &openapiutil.OpenApiRequest{ Headers: headers, Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("GetDomain"), Version: dara.String("2024-03-27"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/v1/domains/" + dara.PercentEncode(dara.StringValue(domainId))), Method: dara.String("GET"), AuthType: dara.String("AK"), Style: dara.String("ROA"), ReqBodyType: dara.String("json"), BodyType: dara.String("json"), } _result = &aliapig.GetDomainResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *ApigClient) ListDomainsWithContext(ctx context.Context, request *aliapig.ListDomainsRequest, headers map[string]*string, runtime *dara.RuntimeOptions) (_result *aliapig.ListDomainsResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.GatewayId) { query["gatewayId"] = request.GatewayId } if !dara.IsNil(request.GatewayType) { query["gatewayType"] = request.GatewayType } if !dara.IsNil(request.NameLike) { query["nameLike"] = request.NameLike } if !dara.IsNil(request.PageNumber) { query["pageNumber"] = request.PageNumber } if !dara.IsNil(request.PageSize) { query["pageSize"] = request.PageSize } if !dara.IsNil(request.ResourceGroupId) { query["resourceGroupId"] = request.ResourceGroupId } req := &openapiutil.OpenApiRequest{ Headers: headers, Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("ListDomains"), Version: dara.String("2024-03-27"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/v1/domains"), Method: dara.String("GET"), AuthType: dara.String("AK"), Style: dara.String("ROA"), ReqBodyType: dara.String("json"), BodyType: dara.String("json"), } _result = &aliapig.ListDomainsResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *ApigClient) UpdateDomainWithContext(ctx context.Context, domainId *string, request *aliapig.UpdateDomainRequest, headers map[string]*string, runtime *dara.RuntimeOptions) (_result *aliapig.UpdateDomainResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } body := map[string]interface{}{} if !dara.IsNil(request.CaCertIdentifier) { body["caCertIdentifier"] = request.CaCertIdentifier } if !dara.IsNil(request.CertIdentifier) { body["certIdentifier"] = request.CertIdentifier } if !dara.IsNil(request.ClientCACert) { body["clientCACert"] = request.ClientCACert } if !dara.IsNil(request.ForceHttps) { body["forceHttps"] = request.ForceHttps } if !dara.IsNil(request.Http2Option) { body["http2Option"] = request.Http2Option } if !dara.IsNil(request.MTLSEnabled) { body["mTLSEnabled"] = request.MTLSEnabled } if !dara.IsNil(request.Protocol) { body["protocol"] = request.Protocol } if !dara.IsNil(request.TlsCipherSuitesConfig) { body["tlsCipherSuitesConfig"] = request.TlsCipherSuitesConfig } if !dara.IsNil(request.TlsMax) { body["tlsMax"] = request.TlsMax } if !dara.IsNil(request.TlsMin) { body["tlsMin"] = request.TlsMin } req := &openapiutil.OpenApiRequest{ Headers: headers, Body: openapiutil.ParseToMap(body), } params := &openapiutil.Params{ Action: dara.String("UpdateDomain"), Version: dara.String("2024-03-27"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/v1/domains/" + dara.PercentEncode(dara.StringValue(domainId))), Method: dara.String("PUT"), AuthType: dara.String("AK"), Style: dara.String("ROA"), ReqBodyType: dara.String("json"), BodyType: dara.String("json"), } _result = &aliapig.UpdateDomainResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } // This is a partial copy of https://github.com/alibabacloud-go/cloudapi-20160714/blob/master/client/client_context_func.go // to lightweight the vendor packages in the built binary. type CloudapiClient struct { openapi.Client DisableSDKError *bool } func NewCloudapiClient(config *openapiutil.Config) (*CloudapiClient, error) { client := new(CloudapiClient) err := client.Init(config) return client, err } func (client *CloudapiClient) DescribeApiGroupWithContext(ctx context.Context, request *alicloudapi.DescribeApiGroupRequest, runtime *dara.RuntimeOptions) (_result *alicloudapi.DescribeApiGroupResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.GroupId) { query["GroupId"] = request.GroupId } if !dara.IsNil(request.SecurityToken) { query["SecurityToken"] = request.SecurityToken } if !dara.IsNil(request.Tag) { query["Tag"] = request.Tag } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("DescribeApiGroup"), Version: dara.String("2016-07-14"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alicloudapi.DescribeApiGroupResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *CloudapiClient) SetDomainCertificateWithContext(ctx context.Context, request *alicloudapi.SetDomainCertificateRequest, runtime *dara.RuntimeOptions) (_result *alicloudapi.SetDomainCertificateResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.CaCertificateBody) { query["CaCertificateBody"] = request.CaCertificateBody } if !dara.IsNil(request.CertificateBody) { query["CertificateBody"] = request.CertificateBody } if !dara.IsNil(request.CertificateName) { query["CertificateName"] = request.CertificateName } if !dara.IsNil(request.CertificatePrivateKey) { query["CertificatePrivateKey"] = request.CertificatePrivateKey } if !dara.IsNil(request.ClientCertSDnPassThrough) { query["ClientCertSDnPassThrough"] = request.ClientCertSDnPassThrough } if !dara.IsNil(request.DomainName) { query["DomainName"] = request.DomainName } if !dara.IsNil(request.GroupId) { query["GroupId"] = request.GroupId } if !dara.IsNil(request.SecurityToken) { query["SecurityToken"] = request.SecurityToken } if !dara.IsNil(request.SslOcspCacheEnable) { query["SslOcspCacheEnable"] = request.SslOcspCacheEnable } if !dara.IsNil(request.SslOcspEnable) { query["SslOcspEnable"] = request.SslOcspEnable } if !dara.IsNil(request.SslVerifyDepth) { query["SslVerifyDepth"] = request.SslVerifyDepth } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("SetDomainCertificate"), Version: dara.String("2016-07-14"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alicloudapi.SetDomainCertificateResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } ================================================ FILE: pkg/core/deployer/providers/aliyun-cas/aliyun_cas.go ================================================ package aliyuncas import ( "context" "errors" "fmt" "log/slog" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas" "github.com/certimate-go/certimate/pkg/core/deployer" ) type DeployerConfig struct { // 阿里云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 阿里云资源组 ID。 ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, ResourceGroupId: config.ResourceGroupId, Region: config.Region, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } return &deployer.DeployResult{}, nil } ================================================ FILE: pkg/core/deployer/providers/aliyun-cas-deploy/aliyun_cas_deploy.go ================================================ package aliyuncasdeploy import ( "context" "errors" "fmt" "log/slog" "strings" "time" alicas "github.com/alibabacloud-go/cas-20200407/v4/client" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" "github.com/alibabacloud-go/tea/dara" "github.com/alibabacloud-go/tea/tea" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-cas-deploy/internal" xwait "github.com/certimate-go/certimate/pkg/utils/wait" ) type DeployerConfig struct { // 阿里云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 阿里云资源组 ID。 ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 云产品资源 ID 数组。 ResourceIds []string `json:"resourceIds"` // 云联系人 ID 数组。 // 零值时使用账号下第一个联系人。 ContactIds []string `json:"contactIds"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.CasClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, ResourceGroupId: config.ResourceGroupId, Region: config.Region, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { if len(d.config.ResourceIds) == 0 { return nil, errors.New("config `resourceIds` is required") } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } contactIds := d.config.ContactIds if len(contactIds) == 0 { // 获取联系人列表 // REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-listcontact listContactReq := &alicas.ListContactRequest{ ShowSize: tea.Int32(1), CurrentPage: tea.Int32(1), } listContactResp, err := d.sdkClient.ListContactWithContext(ctx, listContactReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'cas.ListContact'", slog.Any("request", listContactReq), slog.Any("response", listContactResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cas.ListContact': %w", err) } if len(listContactResp.Body.ContactList) > 0 { contactIds = []string{fmt.Sprintf("%d", listContactResp.Body.ContactList[0].ContactId)} } } // 创建部署任务 // REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-createdeploymentjob createDeploymentJobReq := &alicas.CreateDeploymentJobRequest{ Name: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())), JobType: tea.String("user"), CertIds: tea.String(upres.CertId), ResourceIds: tea.String(strings.Join(d.config.ResourceIds, ",")), ContactIds: tea.String(strings.Join(contactIds, ",")), } createDeploymentJobResp, err := d.sdkClient.CreateDeploymentJobWithContext(ctx, createDeploymentJobReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'cas.CreateDeploymentJob'", slog.Any("request", createDeploymentJobReq), slog.Any("response", createDeploymentJobResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cas.CreateDeploymentJob': %w", err) } // 获取部署任务详情,等待任务状态变更 // REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-describedeploymentjob if _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) { describeDeploymentJobReq := &alicas.DescribeDeploymentJobRequest{ JobId: createDeploymentJobResp.Body.JobId, } describeDeploymentJobResp, err := d.sdkClient.DescribeDeploymentJobWithContext(ctx, describeDeploymentJobReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'cas.DescribeDeploymentJob'", slog.Any("request", describeDeploymentJobReq), slog.Any("response", describeDeploymentJobResp)) if err != nil { return false, fmt.Errorf("failed to execute sdk request 'cas.DescribeDeploymentJob': %w", err) } switch tea.StringValue(describeDeploymentJobResp.Body.Status) { case "success", "error": return true, nil case "", "editing": return false, fmt.Errorf("unexpected aliyun deployment job status") } d.logger.Info("waiting for aliyun deployment job completion ...") return false, nil }, time.Second*5); err != nil { return nil, err } return &deployer.DeployResult{}, nil } func createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.CasClient, error) { // 接入点一览 https://api.aliyun.com/product/cas var endpoint string switch region { case "", "cn-hangzhou": endpoint = "cas.aliyuncs.com" default: endpoint = fmt.Sprintf("cas.%s.aliyuncs.com", region) } config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), Endpoint: tea.String(endpoint), } client, err := internal.NewCasClient(config) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/aliyun-cas-deploy/internal/client.go ================================================ package internal import ( "context" alicas "github.com/alibabacloud-go/cas-20200407/v4/client" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" openapiutil "github.com/alibabacloud-go/darabonba-openapi/v2/utils" "github.com/alibabacloud-go/tea/dara" ) // This is a partial copy of https://github.com/alibabacloud-go/cas-20200407/blob/master/client/client_context_func.go // to lightweight the vendor packages in the built binary. type CasClient struct { openapi.Client DisableSDKError *bool } func NewCasClient(config *openapiutil.Config) (*CasClient, error) { client := new(CasClient) err := client.Init(config) return client, err } func (client *CasClient) Init(config *openapiutil.Config) (_err error) { _err = client.Client.Init(config) if _err != nil { return _err } _err = client.CheckConfig(config) if _err != nil { return _err } return nil } func (client *CasClient) CreateDeploymentJobWithContext(ctx context.Context, request *alicas.CreateDeploymentJobRequest, runtime *dara.RuntimeOptions) (_result *alicas.CreateDeploymentJobResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.CertIds) { query["CertIds"] = request.CertIds } if !dara.IsNil(request.ContactIds) { query["ContactIds"] = request.ContactIds } if !dara.IsNil(request.JobType) { query["JobType"] = request.JobType } if !dara.IsNil(request.Name) { query["Name"] = request.Name } if !dara.IsNil(request.ResourceIds) { query["ResourceIds"] = request.ResourceIds } if !dara.IsNil(request.ScheduleTime) { query["ScheduleTime"] = request.ScheduleTime } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("CreateDeploymentJob"), Version: dara.String("2020-04-07"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alicas.CreateDeploymentJobResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *CasClient) DescribeDeploymentJobWithContext(ctx context.Context, request *alicas.DescribeDeploymentJobRequest, runtime *dara.RuntimeOptions) (_result *alicas.DescribeDeploymentJobResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.JobId) { query["JobId"] = request.JobId } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("DescribeDeploymentJob"), Version: dara.String("2020-04-07"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alicas.DescribeDeploymentJobResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *CasClient) ListContactWithContext(ctx context.Context, request *alicas.ListContactRequest, runtime *dara.RuntimeOptions) (_result *alicas.ListContactResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.CurrentPage) { query["CurrentPage"] = request.CurrentPage } if !dara.IsNil(request.Keyword) { query["Keyword"] = request.Keyword } if !dara.IsNil(request.ShowSize) { query["ShowSize"] = request.ShowSize } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("ListContact"), Version: dara.String("2020-04-07"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alicas.ListContactResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } ================================================ FILE: pkg/core/deployer/providers/aliyun-cdn/aliyun_cdn.go ================================================ package aliyuncdn import ( "context" "errors" "fmt" "log/slog" "strconv" "strings" alicdn "github.com/alibabacloud-go/cdn-20180510/v9/client" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" "github.com/alibabacloud-go/tea/dara" "github.com/alibabacloud-go/tea/tea" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-cdn/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 阿里云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 阿里云资源组 ID。 ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.CdnClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, ResourceGroupId: config.ResourceGroupId, Region: lo. If(config.Region == "" || strings.HasPrefix(config.Region, "cn-"), "cn-hangzhou"). Else("ap-southeast-1"), }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, 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) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } // "*.example.com" → ".example.com",适配阿里云 CDN 要求的泛域名格式 domain := strings.TrimPrefix(d.config.Domain, "*") domains = []string{domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return xcerthostname.IsMatch(d.config.Domain, domain) || strings.TrimPrefix(d.config.Domain, "*") == strings.TrimPrefix(domain, "*") }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by wildcard") } } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil || strings.TrimPrefix(d.config.Domain, "*") == strings.TrimPrefix(domain, "*") }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no cdn domains to deploy") } else { d.logger.Info("found cdn domains to deploy", slog.Any("domains", domains)) var errs []error certIdentifier := upres.ExtendedData["CertIdentifier"].(string) certIdentifierSeps := strings.SplitN(certIdentifier, "-", 2) if len(certIdentifierSeps) != 2 { return nil, fmt.Errorf("received invalid certificate identifier: '%s'", certIdentifier) } certId, _ := strconv.ParseInt(certIdentifierSeps[0], 10, 64) certRegion := certIdentifierSeps[1] for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, certId, certRegion); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 查询域名列表 // REF: https://help.aliyun.com/zh/cdn/developer-reference/api-cdn-2018-05-10-describeuserdomains describeUserDomainsPageNumber := 1 describeUserDomainsPageSize := 500 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } describeUserDomainsReq := &alicdn.DescribeUserDomainsRequest{ ResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId), PageNumber: tea.Int32(int32(describeUserDomainsPageNumber)), PageSize: tea.Int32(int32(describeUserDomainsPageSize)), } describeUserDomainsResp, err := d.sdkClient.DescribeUserDomainsWithContext(ctx, describeUserDomainsReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'cdn.DescribeUserDomains'", slog.Any("request", describeUserDomainsReq), slog.Any("response", describeUserDomainsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.DescribeUserDomains': %w", err) } if describeUserDomainsResp.Body == nil || describeUserDomainsResp.Body.Domains == nil { break } ignoredStatuses := []string{"offline", "checking", "check_failed", "stopping", "deleting"} for _, domainItem := range describeUserDomainsResp.Body.Domains.PageData { if lo.Contains(ignoredStatuses, tea.StringValue(domainItem.DomainStatus)) { continue } domains = append(domains, tea.StringValue(domainItem.DomainName)) } if len(describeUserDomainsResp.Body.Domains.PageData) < describeUserDomainsPageSize { break } describeUserDomainsPageNumber++ } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId int64, certRegion string) error { // 设置 CDN 域名域名证书 // REF: https://help.aliyun.com/zh/cdn/developer-reference/api-cdn-2018-05-10-setcdndomainsslcertificate setCdnDomainSSLCertificateReq := &alicdn.SetCdnDomainSSLCertificateRequest{ DomainName: tea.String(domain), CertType: tea.String("cas"), CertId: tea.Int64(cloudCertId), CertRegion: tea.String(certRegion), SSLProtocol: tea.String("on"), } setCdnDomainSSLCertificateResp, err := d.sdkClient.SetCdnDomainSSLCertificateWithContext(ctx, setCdnDomainSSLCertificateReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'cdn.SetCdnDomainSSLCertificate'", slog.Any("request", setCdnDomainSSLCertificateReq), slog.Any("response", setCdnDomainSSLCertificateResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'cdn.SetCdnDomainSSLCertificate': %w", err) } return nil } func createSDKClient(accessKeyId, accessKeySecret string) (*internal.CdnClient, error) { config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), Endpoint: tea.String("cdn.aliyuncs.com"), } client, err := internal.NewCdnClient(config) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/aliyun-cdn/aliyun_cdn_test.go ================================================ package aliyuncdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-cdn" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fDomain string ) func init() { argsPrefix := "ALIYUNCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./aliyun_cdn_test.go -args \ --ALIYUNCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --ALIYUNCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --ALIYUNCDN_ACCESSKEYID="your-access-key-id" \ --ALIYUNCDN_ACCESSKEYSECRET="your-access-key-secret" \ --ALIYUNCDN_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/aliyun-cdn/consts.go ================================================ package aliyuncdn const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/aliyun-cdn/internal/client.go ================================================ package internal import ( "context" alicdn "github.com/alibabacloud-go/cdn-20180510/v9/client" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" openapiutil "github.com/alibabacloud-go/darabonba-openapi/v2/utils" "github.com/alibabacloud-go/tea/dara" ) // This is a partial copy of https://github.com/alibabacloud-go/cdn-20180510/blob/master/client/client_context_func.go // to lightweight the vendor packages in the built binary. type CdnClient struct { openapi.Client DisableSDKError *bool } func NewCdnClient(config *openapiutil.Config) (*CdnClient, error) { client := new(CdnClient) err := client.Init(config) return client, err } func (client *CdnClient) Init(config *openapiutil.Config) (_err error) { _err = client.Client.Init(config) if _err != nil { return _err } _err = client.CheckConfig(config) if _err != nil { return _err } return nil } func (client *CdnClient) DescribeUserDomainsWithContext(ctx context.Context, request *alicdn.DescribeUserDomainsRequest, runtime *dara.RuntimeOptions) (_result *alicdn.DescribeUserDomainsResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.CdnType) { query["CdnType"] = request.CdnType } if !dara.IsNil(request.ChangeEndTime) { query["ChangeEndTime"] = request.ChangeEndTime } if !dara.IsNil(request.ChangeStartTime) { query["ChangeStartTime"] = request.ChangeStartTime } if !dara.IsNil(request.CheckDomainShow) { query["CheckDomainShow"] = request.CheckDomainShow } if !dara.IsNil(request.Coverage) { query["Coverage"] = request.Coverage } if !dara.IsNil(request.DomainName) { query["DomainName"] = request.DomainName } if !dara.IsNil(request.DomainSearchType) { query["DomainSearchType"] = request.DomainSearchType } if !dara.IsNil(request.DomainStatus) { query["DomainStatus"] = request.DomainStatus } if !dara.IsNil(request.OwnerId) { query["OwnerId"] = request.OwnerId } if !dara.IsNil(request.PageNumber) { query["PageNumber"] = request.PageNumber } if !dara.IsNil(request.PageSize) { query["PageSize"] = request.PageSize } if !dara.IsNil(request.ResourceGroupId) { query["ResourceGroupId"] = request.ResourceGroupId } if !dara.IsNil(request.SecurityToken) { query["SecurityToken"] = request.SecurityToken } if !dara.IsNil(request.Source) { query["Source"] = request.Source } if !dara.IsNil(request.Tag) { query["Tag"] = request.Tag } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("DescribeUserDomains"), Version: dara.String("2018-05-10"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alicdn.DescribeUserDomainsResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *CdnClient) SetCdnDomainSSLCertificateWithContext(ctx context.Context, request *alicdn.SetCdnDomainSSLCertificateRequest, runtime *dara.RuntimeOptions) (_result *alicdn.SetCdnDomainSSLCertificateResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.CertId) { query["CertId"] = request.CertId } if !dara.IsNil(request.CertName) { query["CertName"] = request.CertName } if !dara.IsNil(request.CertRegion) { query["CertRegion"] = request.CertRegion } if !dara.IsNil(request.CertType) { query["CertType"] = request.CertType } if !dara.IsNil(request.DomainName) { query["DomainName"] = request.DomainName } if !dara.IsNil(request.OwnerId) { query["OwnerId"] = request.OwnerId } if !dara.IsNil(request.SSLPri) { query["SSLPri"] = request.SSLPri } if !dara.IsNil(request.SSLProtocol) { query["SSLProtocol"] = request.SSLProtocol } if !dara.IsNil(request.SSLPub) { query["SSLPub"] = request.SSLPub } if !dara.IsNil(request.SecurityToken) { query["SecurityToken"] = request.SecurityToken } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("SetCdnDomainSSLCertificate"), Version: dara.String("2018-05-10"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alicdn.SetCdnDomainSSLCertificateResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } ================================================ FILE: pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go ================================================ package aliyunclb import ( "context" "errors" "fmt" "log/slog" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" alislb "github.com/alibabacloud-go/slb-20140515/v4/client" "github.com/alibabacloud-go/tea/dara" "github.com/alibabacloud-go/tea/tea" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-slb" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-clb/internal" ) type DeployerConfig struct { // 阿里云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 阿里云资源组 ID。 ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 负载均衡实例 ID。 // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时必填。 LoadbalancerId string `json:"loadbalancerId,omitempty"` // 负载均衡监听端口。 // 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。 ListenerPort int32 `json:"listenerPort,omitempty"` // SNI 域名(支持泛域名)。 // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时选填。 Domain string `json:"domain,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.SlbClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, ResourceGroupId: config.ResourceGroupId, Region: config.Region, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_LOADBALANCER: if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil { return nil, err } case RESOURCE_TYPE_LISTENER: if err := d.deployToListener(ctx, upres.CertId); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error { if d.config.LoadbalancerId == "" { return errors.New("config `loadbalancerId` is required") } // 查询负载均衡实例的详细信息 // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerattribute describeLoadBalancerAttributeReq := &alislb.DescribeLoadBalancerAttributeRequest{ RegionId: tea.String(d.config.Region), LoadBalancerId: tea.String(d.config.LoadbalancerId), } describeLoadBalancerAttributeResp, err := d.sdkClient.DescribeLoadBalancerAttributeWithContext(ctx, describeLoadBalancerAttributeReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'slb.DescribeLoadBalancerAttribute'", slog.Any("request", describeLoadBalancerAttributeReq), slog.Any("response", describeLoadBalancerAttributeResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerAttribute': %w", err) } // 查询 HTTPS 监听列表 // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerlisteners listenerPorts := make([]int32, 0) describeLoadBalancerListenersToken := (*string)(nil) for { select { case <-ctx.Done(): return ctx.Err() default: } describeLoadBalancerListenersReq := &alislb.DescribeLoadBalancerListenersRequest{ RegionId: tea.String(d.config.Region), NextToken: describeLoadBalancerListenersToken, MaxResults: tea.Int32(100), LoadBalancerId: tea.StringSlice([]string{d.config.LoadbalancerId}), ListenerProtocol: tea.String("https"), } describeLoadBalancerListenersResp, err := d.sdkClient.DescribeLoadBalancerListenersWithContext(ctx, describeLoadBalancerListenersReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'slb.DescribeLoadBalancerListeners'", slog.Any("request", describeLoadBalancerListenersReq), slog.Any("response", describeLoadBalancerListenersResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerListeners': %w", err) } if describeLoadBalancerListenersResp.Body == nil { break } for _, listener := range describeLoadBalancerListenersResp.Body.Listeners { listenerPorts = append(listenerPorts, *listener.ListenerPort) } if len(describeLoadBalancerListenersResp.Body.Listeners) == 0 || describeLoadBalancerListenersResp.Body.NextToken == nil { break } describeLoadBalancerListenersToken = describeLoadBalancerListenersResp.Body.NextToken } // 遍历更新监听证书 if len(listenerPorts) == 0 { d.logger.Info("no clb listeners to deploy") } else { d.logger.Info("found https listeners to deploy", slog.Any("listenerPorts", listenerPorts)) var errs []error for _, listenerPort := range listenerPorts { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listenerPort, cloudCertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error { if d.config.LoadbalancerId == "" { return errors.New("config `loadbalancerId` is required") } if d.config.ListenerPort == 0 { return errors.New("config `listenerPort` is required") } // 更新监听 if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, d.config.ListenerPort, cloudCertId); err != nil { return err } return nil } func (d *Deployer) updateListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudListenerPort int32, cloudCertId string) error { // 查询监听配置 // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerhttpslistenerattribute describeLoadBalancerHTTPSListenerAttributeReq := &alislb.DescribeLoadBalancerHTTPSListenerAttributeRequest{ LoadBalancerId: tea.String(cloudLoadbalancerId), ListenerPort: tea.Int32(cloudListenerPort), } describeLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.DescribeLoadBalancerHTTPSListenerAttributeWithContext(ctx, describeLoadBalancerHTTPSListenerAttributeReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'slb.DescribeLoadBalancerHTTPSListenerAttribute'", slog.Any("request", describeLoadBalancerHTTPSListenerAttributeReq), slog.Any("response", describeLoadBalancerHTTPSListenerAttributeResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerHTTPSListenerAttribute': %w", err) } if d.config.Domain == "" { // 未指定 SNI,只需部署到监听器 // 修改监听配置 // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setloadbalancerhttpslistenerattribute setLoadBalancerHTTPSListenerAttributeReq := &alislb.SetLoadBalancerHTTPSListenerAttributeRequest{ RegionId: tea.String(d.config.Region), LoadBalancerId: tea.String(cloudLoadbalancerId), ListenerPort: tea.Int32(cloudListenerPort), ServerCertificateId: tea.String(cloudCertId), } setLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.SetLoadBalancerHTTPSListenerAttributeWithContext(ctx, setLoadBalancerHTTPSListenerAttributeReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'slb.SetLoadBalancerHTTPSListenerAttribute'", slog.Any("request", setLoadBalancerHTTPSListenerAttributeReq), slog.Any("response", setLoadBalancerHTTPSListenerAttributeResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'slb.SetLoadBalancerHTTPSListenerAttribute': %w", err) } } else { // 指定 SNI,需部署到扩展域名 // 查询扩展域名 // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describedomainextensions describeDomainExtensionsReq := &alislb.DescribeDomainExtensionsRequest{ RegionId: tea.String(d.config.Region), LoadBalancerId: tea.String(cloudLoadbalancerId), ListenerPort: tea.Int32(cloudListenerPort), } describeDomainExtensionsResp, err := d.sdkClient.DescribeDomainExtensionsWithContext(ctx, describeDomainExtensionsReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'slb.DescribeDomainExtensions'", slog.Any("request", describeDomainExtensionsReq), slog.Any("response", describeDomainExtensionsResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'slb.DescribeDomainExtensions': %w", err) } // 遍历修改扩展域名证书 // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setdomainextensionattribute if describeDomainExtensionsResp.Body.DomainExtensions != nil && describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension != nil { var errs []error for _, domainExtension := range describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension { if *domainExtension.Domain != d.config.Domain { continue } setDomainExtensionAttributeReq := &alislb.SetDomainExtensionAttributeRequest{ RegionId: tea.String(d.config.Region), DomainExtensionId: tea.String(*domainExtension.DomainExtensionId), ServerCertificateId: tea.String(cloudCertId), } setDomainExtensionAttributeResp, err := d.sdkClient.SetDomainExtensionAttributeWithContext(ctx, setDomainExtensionAttributeReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'slb.SetDomainExtensionAttribute'", slog.Any("request", setDomainExtensionAttributeReq), slog.Any("response", setDomainExtensionAttributeResp)) if err != nil { errs = append(errs, fmt.Errorf("failed to execute sdk request 'slb.SetDomainExtensionAttribute': %w", err)) continue } } if len(errs) > 0 { return errors.Join(errs...) } } } return nil } func createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.SlbClient, error) { // 接入点一览 https://api.aliyun.com/product/Slb var endpoint string switch region { case "", "cn-hangzhou", "cn-hangzhou-finance", "cn-shanghai-finance-1", "cn-shenzhen-finance-1": endpoint = "slb.aliyuncs.com" default: endpoint = fmt.Sprintf("slb.%s.aliyuncs.com", region) } config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), Endpoint: tea.String(endpoint), } client, err := internal.NewSlbClient(config) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/aliyun-clb/aliyun_clb_test.go ================================================ package aliyunclb_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-clb" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fRegion string fLoadbalancerId string fListenerPort int64 fDomain string ) func init() { argsPrefix := "ALIYUNCLB_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "") flag.Int64Var(&fListenerPort, argsPrefix+"LISTENERPORT", 443, "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./aliyun_clb_test.go -args \ --ALIYUNCLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --ALIYUNCLB_INPUTKEYPATH="/path/to/your-input-key.pem" \ --ALIYUNCLB_ACCESSKEYID="your-access-key-id" \ --ALIYUNCLB_ACCESSKEYSECRET="your-access-key-secret" \ --ALIYUNCLB_REGION="cn-hangzhou" \ --ALIYUNCLB_LOADBALANCERID="your-clb-instance-id" \ --ALIYUNCLB_LISTENERPORT=443 \ --ALIYUNCLB_DOMAIN="your-clb-sni-domain" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy_ToLoadbalancer", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Region: fRegion, ResourceType: provider.RESOURCE_TYPE_LOADBALANCER, LoadbalancerId: fLoadbalancerId, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) t.Run("Deploy_ToListener", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId), fmt.Sprintf("LISTENERPORT: %v", fListenerPort), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Region: fRegion, ResourceType: provider.RESOURCE_TYPE_LISTENER, LoadbalancerId: fLoadbalancerId, ListenerPort: int32(fListenerPort), Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/aliyun-clb/consts.go ================================================ package aliyunclb const ( // 资源类型:部署到指定负载均衡器。 RESOURCE_TYPE_LOADBALANCER = "loadbalancer" // 资源类型:部署到指定监听器。 RESOURCE_TYPE_LISTENER = "listener" ) ================================================ FILE: pkg/core/deployer/providers/aliyun-clb/internal/client.go ================================================ package internal import ( "context" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" openapiutil "github.com/alibabacloud-go/darabonba-openapi/v2/utils" alislb "github.com/alibabacloud-go/slb-20140515/v4/client" "github.com/alibabacloud-go/tea/dara" ) // This is a partial copy of https://github.com/alibabacloud-go/slb-20140515/blob/master/client/client_context_func.go // to lightweight the vendor packages in the built binary. type SlbClient struct { openapi.Client } func NewSlbClient(config *openapi.Config) (*SlbClient, error) { client := new(SlbClient) err := client.Init(config) return client, err } func (client *SlbClient) Init(config *openapi.Config) (_err error) { _err = client.Client.Init(config) if _err != nil { return _err } _err = client.CheckConfig(config) if _err != nil { return _err } return nil } func (client *SlbClient) DescribeDomainExtensionsWithContext(ctx context.Context, request *alislb.DescribeDomainExtensionsRequest, runtime *dara.RuntimeOptions) (_result *alislb.DescribeDomainExtensionsResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.DomainExtensionId) { query["DomainExtensionId"] = request.DomainExtensionId } if !dara.IsNil(request.ListenerPort) { query["ListenerPort"] = request.ListenerPort } if !dara.IsNil(request.LoadBalancerId) { query["LoadBalancerId"] = request.LoadBalancerId } if !dara.IsNil(request.OwnerAccount) { query["OwnerAccount"] = request.OwnerAccount } if !dara.IsNil(request.OwnerId) { query["OwnerId"] = request.OwnerId } if !dara.IsNil(request.RegionId) { query["RegionId"] = request.RegionId } if !dara.IsNil(request.ResourceOwnerAccount) { query["ResourceOwnerAccount"] = request.ResourceOwnerAccount } if !dara.IsNil(request.ResourceOwnerId) { query["ResourceOwnerId"] = request.ResourceOwnerId } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("DescribeDomainExtensions"), Version: dara.String("2014-05-15"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alislb.DescribeDomainExtensionsResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *SlbClient) DescribeLoadBalancerListenersWithContext(ctx context.Context, request *alislb.DescribeLoadBalancerListenersRequest, runtime *dara.RuntimeOptions) (_result *alislb.DescribeLoadBalancerListenersResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.Description) { query["Description"] = request.Description } if !dara.IsNil(request.ListenerPort) { query["ListenerPort"] = request.ListenerPort } if !dara.IsNil(request.ListenerProtocol) { query["ListenerProtocol"] = request.ListenerProtocol } if !dara.IsNil(request.LoadBalancerId) { query["LoadBalancerId"] = request.LoadBalancerId } if !dara.IsNil(request.MaxResults) { query["MaxResults"] = request.MaxResults } if !dara.IsNil(request.NextToken) { query["NextToken"] = request.NextToken } if !dara.IsNil(request.OwnerAccount) { query["OwnerAccount"] = request.OwnerAccount } if !dara.IsNil(request.OwnerId) { query["OwnerId"] = request.OwnerId } if !dara.IsNil(request.RegionId) { query["RegionId"] = request.RegionId } if !dara.IsNil(request.ResourceOwnerAccount) { query["ResourceOwnerAccount"] = request.ResourceOwnerAccount } if !dara.IsNil(request.ResourceOwnerId) { query["ResourceOwnerId"] = request.ResourceOwnerId } if !dara.IsNil(request.Tag) { query["Tag"] = request.Tag } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("DescribeLoadBalancerListeners"), Version: dara.String("2014-05-15"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alislb.DescribeLoadBalancerListenersResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *SlbClient) DescribeLoadBalancerAttributeWithContext(ctx context.Context, request *alislb.DescribeLoadBalancerAttributeRequest, runtime *dara.RuntimeOptions) (_result *alislb.DescribeLoadBalancerAttributeResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.LoadBalancerId) { query["LoadBalancerId"] = request.LoadBalancerId } if !dara.IsNil(request.OwnerAccount) { query["OwnerAccount"] = request.OwnerAccount } if !dara.IsNil(request.OwnerId) { query["OwnerId"] = request.OwnerId } if !dara.IsNil(request.RegionId) { query["RegionId"] = request.RegionId } if !dara.IsNil(request.ResourceOwnerAccount) { query["ResourceOwnerAccount"] = request.ResourceOwnerAccount } if !dara.IsNil(request.ResourceOwnerId) { query["ResourceOwnerId"] = request.ResourceOwnerId } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("DescribeLoadBalancerAttribute"), Version: dara.String("2014-05-15"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alislb.DescribeLoadBalancerAttributeResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *SlbClient) DescribeLoadBalancerHTTPSListenerAttributeWithContext(ctx context.Context, request *alislb.DescribeLoadBalancerHTTPSListenerAttributeRequest, runtime *dara.RuntimeOptions) (_result *alislb.DescribeLoadBalancerHTTPSListenerAttributeResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.ListenerPort) { query["ListenerPort"] = request.ListenerPort } if !dara.IsNil(request.LoadBalancerId) { query["LoadBalancerId"] = request.LoadBalancerId } if !dara.IsNil(request.OwnerAccount) { query["OwnerAccount"] = request.OwnerAccount } if !dara.IsNil(request.OwnerId) { query["OwnerId"] = request.OwnerId } if !dara.IsNil(request.RegionId) { query["RegionId"] = request.RegionId } if !dara.IsNil(request.ResourceOwnerAccount) { query["ResourceOwnerAccount"] = request.ResourceOwnerAccount } if !dara.IsNil(request.ResourceOwnerId) { query["ResourceOwnerId"] = request.ResourceOwnerId } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("DescribeLoadBalancerHTTPSListenerAttribute"), Version: dara.String("2014-05-15"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alislb.DescribeLoadBalancerHTTPSListenerAttributeResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *SlbClient) SetDomainExtensionAttributeWithContext(ctx context.Context, request *alislb.SetDomainExtensionAttributeRequest, runtime *dara.RuntimeOptions) (_result *alislb.SetDomainExtensionAttributeResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.DomainExtensionId) { query["DomainExtensionId"] = request.DomainExtensionId } if !dara.IsNil(request.OwnerAccount) { query["OwnerAccount"] = request.OwnerAccount } if !dara.IsNil(request.OwnerId) { query["OwnerId"] = request.OwnerId } if !dara.IsNil(request.RegionId) { query["RegionId"] = request.RegionId } if !dara.IsNil(request.ResourceOwnerAccount) { query["ResourceOwnerAccount"] = request.ResourceOwnerAccount } if !dara.IsNil(request.ResourceOwnerId) { query["ResourceOwnerId"] = request.ResourceOwnerId } if !dara.IsNil(request.ServerCertificateId) { query["ServerCertificateId"] = request.ServerCertificateId } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("SetDomainExtensionAttribute"), Version: dara.String("2014-05-15"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alislb.SetDomainExtensionAttributeResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *SlbClient) SetLoadBalancerHTTPSListenerAttributeWithContext(ctx context.Context, request *alislb.SetLoadBalancerHTTPSListenerAttributeRequest, runtime *dara.RuntimeOptions) (_result *alislb.SetLoadBalancerHTTPSListenerAttributeResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.AclId) { query["AclId"] = request.AclId } if !dara.IsNil(request.AclStatus) { query["AclStatus"] = request.AclStatus } if !dara.IsNil(request.AclType) { query["AclType"] = request.AclType } if !dara.IsNil(request.Bandwidth) { query["Bandwidth"] = request.Bandwidth } if !dara.IsNil(request.CACertificateId) { query["CACertificateId"] = request.CACertificateId } if !dara.IsNil(request.Cookie) { query["Cookie"] = request.Cookie } if !dara.IsNil(request.CookieTimeout) { query["CookieTimeout"] = request.CookieTimeout } if !dara.IsNil(request.Description) { query["Description"] = request.Description } if !dara.IsNil(request.EnableHttp2) { query["EnableHttp2"] = request.EnableHttp2 } if !dara.IsNil(request.Gzip) { query["Gzip"] = request.Gzip } if !dara.IsNil(request.HealthCheck) { query["HealthCheck"] = request.HealthCheck } if !dara.IsNil(request.HealthCheckConnectPort) { query["HealthCheckConnectPort"] = request.HealthCheckConnectPort } if !dara.IsNil(request.HealthCheckDomain) { query["HealthCheckDomain"] = request.HealthCheckDomain } if !dara.IsNil(request.HealthCheckHttpCode) { query["HealthCheckHttpCode"] = request.HealthCheckHttpCode } if !dara.IsNil(request.HealthCheckInterval) { query["HealthCheckInterval"] = request.HealthCheckInterval } if !dara.IsNil(request.HealthCheckMethod) { query["HealthCheckMethod"] = request.HealthCheckMethod } if !dara.IsNil(request.HealthCheckTimeout) { query["HealthCheckTimeout"] = request.HealthCheckTimeout } if !dara.IsNil(request.HealthCheckURI) { query["HealthCheckURI"] = request.HealthCheckURI } if !dara.IsNil(request.HealthyThreshold) { query["HealthyThreshold"] = request.HealthyThreshold } if !dara.IsNil(request.IdleTimeout) { query["IdleTimeout"] = request.IdleTimeout } if !dara.IsNil(request.ListenerPort) { query["ListenerPort"] = request.ListenerPort } if !dara.IsNil(request.LoadBalancerId) { query["LoadBalancerId"] = request.LoadBalancerId } if !dara.IsNil(request.OwnerAccount) { query["OwnerAccount"] = request.OwnerAccount } if !dara.IsNil(request.OwnerId) { query["OwnerId"] = request.OwnerId } if !dara.IsNil(request.RegionId) { query["RegionId"] = request.RegionId } if !dara.IsNil(request.RequestTimeout) { query["RequestTimeout"] = request.RequestTimeout } if !dara.IsNil(request.ResourceOwnerAccount) { query["ResourceOwnerAccount"] = request.ResourceOwnerAccount } if !dara.IsNil(request.ResourceOwnerId) { query["ResourceOwnerId"] = request.ResourceOwnerId } if !dara.IsNil(request.Scheduler) { query["Scheduler"] = request.Scheduler } if !dara.IsNil(request.ServerCertificateId) { query["ServerCertificateId"] = request.ServerCertificateId } if !dara.IsNil(request.StickySession) { query["StickySession"] = request.StickySession } if !dara.IsNil(request.StickySessionType) { query["StickySessionType"] = request.StickySessionType } if !dara.IsNil(request.TLSCipherPolicy) { query["TLSCipherPolicy"] = request.TLSCipherPolicy } if !dara.IsNil(request.UnhealthyThreshold) { query["UnhealthyThreshold"] = request.UnhealthyThreshold } if !dara.IsNil(request.VServerGroup) { query["VServerGroup"] = request.VServerGroup } if !dara.IsNil(request.VServerGroupId) { query["VServerGroupId"] = request.VServerGroupId } if !dara.IsNil(request.XForwardedFor) { query["XForwardedFor"] = request.XForwardedFor } if !dara.IsNil(request.XForwardedFor_ClientSrcPort) { query["XForwardedFor_ClientSrcPort"] = request.XForwardedFor_ClientSrcPort } if !dara.IsNil(request.XForwardedFor_SLBID) { query["XForwardedFor_SLBID"] = request.XForwardedFor_SLBID } if !dara.IsNil(request.XForwardedFor_SLBIP) { query["XForwardedFor_SLBIP"] = request.XForwardedFor_SLBIP } if !dara.IsNil(request.XForwardedFor_SLBPORT) { query["XForwardedFor_SLBPORT"] = request.XForwardedFor_SLBPORT } if !dara.IsNil(request.XForwardedFor_proto) { query["XForwardedFor_proto"] = request.XForwardedFor_proto } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("SetLoadBalancerHTTPSListenerAttribute"), Version: dara.String("2014-05-15"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alislb.SetLoadBalancerHTTPSListenerAttributeResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } ================================================ FILE: pkg/core/deployer/providers/aliyun-dcdn/aliyun_dcdn.go ================================================ package aliyundcdn import ( "context" "errors" "fmt" "log/slog" "strconv" "strings" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" alidcdn "github.com/alibabacloud-go/dcdn-20180115/v4/client" "github.com/alibabacloud-go/tea/dara" "github.com/alibabacloud-go/tea/tea" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-dcdn/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 阿里云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 阿里云资源组 ID。 ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.DcdnClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, ResourceGroupId: config.ResourceGroupId, Region: lo. If(config.Region == "" || strings.HasPrefix(config.Region, "cn-"), "cn-hangzhou"). Else("ap-southeast-1"), }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, 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) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } // "*.example.com" → ".example.com",适配阿里云 DCDN 要求的泛域名格式 domain := strings.TrimPrefix(d.config.Domain, "*") domains = []string{domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return xcerthostname.IsMatch(d.config.Domain, domain) || strings.TrimPrefix(d.config.Domain, "*") == strings.TrimPrefix(domain, "*") }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by wildcard") } } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil || strings.TrimPrefix(d.config.Domain, "*") == strings.TrimPrefix(domain, "*") }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no dcdn domains to deploy") } else { d.logger.Info("found dcdn domains to deploy", slog.Any("domains", domains)) var errs []error certIdentifier := upres.ExtendedData["CertIdentifier"].(string) certIdentifierSeps := strings.SplitN(certIdentifier, "-", 2) if len(certIdentifierSeps) != 2 { return nil, fmt.Errorf("received invalid certificate identifier: '%s'", certIdentifier) } certId, _ := strconv.ParseInt(certIdentifierSeps[0], 10, 64) certRegion := certIdentifierSeps[1] for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, certId, certRegion); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 查询域名列表 // REF: https://help.aliyun.com/zh/edge-security-acceleration/dcdn/developer-reference/api-dcdn-2018-01-15-describedcdnuserdomains describeUserDomainsPageNumber := 1 describeUserDomainsPageSize := 500 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } describeDcdnUserDomainsReq := &alidcdn.DescribeDcdnUserDomainsRequest{ ResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId), CheckDomainShow: tea.Bool(true), PageNumber: tea.Int32(int32(describeUserDomainsPageNumber)), PageSize: tea.Int32(int32(describeUserDomainsPageSize)), } describeDcdnUserDomainsResp, err := d.sdkClient.DescribeDcdnUserDomainsWithContext(ctx, describeDcdnUserDomainsReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'dcdn.DescribeDcdnUserDomains'", slog.Any("request", describeDcdnUserDomainsReq), slog.Any("response", describeDcdnUserDomainsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'dcdn.DescribeDcdnUserDomains': %w", err) } if describeDcdnUserDomainsResp.Body == nil || describeDcdnUserDomainsResp.Body.Domains == nil { break } ignoredStatuses := []string{"offline", "checking", "check_failed", "stopping", "deleting"} for _, domainItem := range describeDcdnUserDomainsResp.Body.Domains.PageData { if lo.Contains(ignoredStatuses, tea.StringValue(domainItem.DomainStatus)) { continue } domains = append(domains, tea.StringValue(domainItem.DomainName)) } if len(describeDcdnUserDomainsResp.Body.Domains.PageData) < describeUserDomainsPageNumber { break } describeUserDomainsPageNumber++ } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId int64, certRegion string) error { // 配置域名证书 // REF: https://help.aliyun.com/zh/edge-security-acceleration/dcdn/developer-reference/api-dcdn-2018-01-15-setdcdndomainsslcertificate setDcdnDomainSSLCertificateReq := &alidcdn.SetDcdnDomainSSLCertificateRequest{ DomainName: tea.String(domain), CertType: tea.String("cas"), CertId: tea.Int64(cloudCertId), CertRegion: tea.String(certRegion), SSLProtocol: tea.String("on"), } setDcdnDomainSSLCertificateResp, err := d.sdkClient.SetDcdnDomainSSLCertificateWithContext(ctx, setDcdnDomainSSLCertificateReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'dcdn.SetDcdnDomainSSLCertificate'", slog.Any("request", setDcdnDomainSSLCertificateReq), slog.Any("response", setDcdnDomainSSLCertificateResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'dcdn.SetDcdnDomainSSLCertificate': %w", err) } return nil } func createSDKClient(accessKeyId, accessKeySecret string) (*internal.DcdnClient, error) { config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), Endpoint: tea.String("dcdn.aliyuncs.com"), } client, err := internal.NewDcdnClient(config) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/aliyun-dcdn/aliyun_dcdn_test.go ================================================ package aliyundcdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-dcdn" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fDomain string ) func init() { argsPrefix := "ALIYUNDCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./aliyun_dcdn_test.go -args \ --ALIYUNDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --ALIYUNDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --ALIYUNDCDN_ACCESSKEYID="your-access-key-id" \ --ALIYUNDCDN_ACCESSKEYSECRET="your-access-key-secret" \ --ALIYUNDCDN_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/aliyun-dcdn/consts.go ================================================ package aliyundcdn const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/aliyun-dcdn/internal/client.go ================================================ package internal import ( "context" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" openapiutil "github.com/alibabacloud-go/darabonba-openapi/v2/utils" alidcdn "github.com/alibabacloud-go/dcdn-20180115/v4/client" "github.com/alibabacloud-go/tea/dara" ) // This is a partial copy of https://github.com/alibabacloud-go/dcdn-20180115/blob/master/client/client_context_func.go // to lightweight the vendor packages in the built binary. type DcdnClient struct { openapi.Client DisableSDKError *bool } func NewDcdnClient(config *openapiutil.Config) (*DcdnClient, error) { client := new(DcdnClient) err := client.Init(config) return client, err } func (client *DcdnClient) Init(config *openapiutil.Config) (_err error) { _err = client.Client.Init(config) if _err != nil { return _err } _err = client.CheckConfig(config) if _err != nil { return _err } return nil } func (client *DcdnClient) DescribeDcdnUserDomainsWithContext(ctx context.Context, request *alidcdn.DescribeDcdnUserDomainsRequest, runtime *dara.RuntimeOptions) (_result *alidcdn.DescribeDcdnUserDomainsResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.ChangeEndTime) { query["ChangeEndTime"] = request.ChangeEndTime } if !dara.IsNil(request.ChangeStartTime) { query["ChangeStartTime"] = request.ChangeStartTime } if !dara.IsNil(request.CheckDomainShow) { query["CheckDomainShow"] = request.CheckDomainShow } if !dara.IsNil(request.Coverage) { query["Coverage"] = request.Coverage } if !dara.IsNil(request.DomainName) { query["DomainName"] = request.DomainName } if !dara.IsNil(request.DomainSearchType) { query["DomainSearchType"] = request.DomainSearchType } if !dara.IsNil(request.DomainStatus) { query["DomainStatus"] = request.DomainStatus } if !dara.IsNil(request.OwnerId) { query["OwnerId"] = request.OwnerId } if !dara.IsNil(request.PageNumber) { query["PageNumber"] = request.PageNumber } if !dara.IsNil(request.PageSize) { query["PageSize"] = request.PageSize } if !dara.IsNil(request.ResourceGroupId) { query["ResourceGroupId"] = request.ResourceGroupId } if !dara.IsNil(request.SecurityToken) { query["SecurityToken"] = request.SecurityToken } if !dara.IsNil(request.Tag) { query["Tag"] = request.Tag } if !dara.IsNil(request.WebSiteType) { query["WebSiteType"] = request.WebSiteType } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("DescribeDcdnUserDomains"), Version: dara.String("2018-01-15"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alidcdn.DescribeDcdnUserDomainsResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *DcdnClient) SetDcdnDomainSSLCertificateWithContext(ctx context.Context, request *alidcdn.SetDcdnDomainSSLCertificateRequest, runtime *dara.RuntimeOptions) (_result *alidcdn.SetDcdnDomainSSLCertificateResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.CertId) { query["CertId"] = request.CertId } if !dara.IsNil(request.CertName) { query["CertName"] = request.CertName } if !dara.IsNil(request.CertRegion) { query["CertRegion"] = request.CertRegion } if !dara.IsNil(request.CertType) { query["CertType"] = request.CertType } if !dara.IsNil(request.DomainName) { query["DomainName"] = request.DomainName } if !dara.IsNil(request.OwnerId) { query["OwnerId"] = request.OwnerId } if !dara.IsNil(request.SSLPri) { query["SSLPri"] = request.SSLPri } if !dara.IsNil(request.SSLProtocol) { query["SSLProtocol"] = request.SSLProtocol } if !dara.IsNil(request.SSLPub) { query["SSLPub"] = request.SSLPub } if !dara.IsNil(request.SecurityToken) { query["SecurityToken"] = request.SecurityToken } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("SetDcdnDomainSSLCertificate"), Version: dara.String("2018-01-15"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alidcdn.SetDcdnDomainSSLCertificateResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } ================================================ FILE: pkg/core/deployer/providers/aliyun-ddospro/aliyun_ddospro.go ================================================ package aliyunddospro import ( "context" "errors" "fmt" "log/slog" "strings" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" aliddoscoo "github.com/alibabacloud-go/ddoscoo-20200101/v5/client" "github.com/alibabacloud-go/tea/dara" "github.com/alibabacloud-go/tea/tea" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-ddospro/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 阿里云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 阿里云资源组 ID。 ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 网站域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.DdoscooClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, ResourceGroupId: config.ResourceGroupId, Region: lo. If(config.Region == "" || strings.HasPrefix(config.Region, "cn-"), "cn-hangzhou"). Else("ap-southeast-1"), }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return xcerthostname.IsMatch(d.config.Domain, domain) }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by wildcard") } } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no ddoscoo domains to deploy") } else { d.logger.Info("found ddoscoo domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: certId := upres.ExtendedData["CertIdentifier"].(string) if err := d.updateDomainCertificate(ctx, domain, certId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 查询已配置网站业务转发规则的域名 // REF: https://help.aliyun.com/zh/anti-ddos/anti-ddos-pro-and-premium/developer-reference/api-ddoscoo-2020-01-01-describedomains describeDomainsReq := &aliddoscoo.DescribeDomainsRequest{ ResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId), } describeDomainsResp, err := d.sdkClient.DescribeDomainsWithContext(ctx, describeDomainsReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'aliddoscoo.DescribeLiveUserDomains'", slog.Any("request", describeDomainsReq), slog.Any("response", describeDomainsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'aliddoscoo.DescribeDomains': %w", err) } for _, domain := range describeDomainsResp.Body.Domains { domains = append(domains, tea.StringValue(domain)) } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error { // 为网站业务转发规则关联 SSL 证书 // REF: https://help.aliyun.com/zh/anti-ddos/anti-ddos-pro-and-premium/developer-reference/api-ddoscoo-2020-01-01-associatewebcert associateWebCertReq := &aliddoscoo.AssociateWebCertRequest{ Domain: tea.String(domain), CertIdentifier: tea.String(cloudCertId), } associateWebCertResp, err := d.sdkClient.AssociateWebCertWithContext(ctx, associateWebCertReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'dcdn.AssociateWebCert'", slog.Any("request", associateWebCertReq), slog.Any("response", associateWebCertResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'dcdn.AssociateWebCert': %w", err) } return nil } func createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.DdoscooClient, error) { // 接入点一览 https://api.aliyun.com/product/ddoscoo var endpoint string switch region { case "": endpoint = "ddoscoo.cn-hangzhou.aliyuncs.com" default: endpoint = fmt.Sprintf("ddoscoo.%s.aliyuncs.com", region) } config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), Endpoint: tea.String(endpoint), } client, err := internal.NewDdoscooClient(config) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/aliyun-ddospro/aliyun_ddospro_test.go ================================================ package aliyunddospro_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-ddospro" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fRegion string fDomain string ) func init() { argsPrefix := "ALIYUNDDOSPRO_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./aliyun_ddospro_test.go -args \ --ALIYUNDDOSPRO_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --ALIYUNDDOSPRO_INPUTKEYPATH="/path/to/your-input-key.pem" \ --ALIYUNDDOSPRO_ACCESSKEYID="your-access-key-id" \ --ALIYUNDDOSPRO_ACCESSKEYSECRET="your-access-key-secret" \ --ALIYUNDDOSPRO_REGION="cn-hangzhou" \ --ALIYUNDDOSPRO_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Region: fRegion, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/aliyun-ddospro/consts.go ================================================ package aliyunddospro const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/aliyun-ddospro/internal/client.go ================================================ package internal import ( "context" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" openapiutil "github.com/alibabacloud-go/darabonba-openapi/v2/utils" aliddoscoo "github.com/alibabacloud-go/ddoscoo-20200101/v5/client" "github.com/alibabacloud-go/tea/dara" ) // This is a partial copy of https://github.com/alibabacloud-go/ddoscoo-20200101/blob/master/client/client_context_func.go // to lightweight the vendor packages in the built binary. type DdoscooClient struct { openapi.Client DisableSDKError *bool } func NewDdoscooClient(config *openapiutil.Config) (*DdoscooClient, error) { client := new(DdoscooClient) err := client.Init(config) return client, err } func (client *DdoscooClient) Init(config *openapiutil.Config) (_err error) { _err = client.Client.Init(config) if _err != nil { return _err } _err = client.CheckConfig(config) if _err != nil { return _err } return nil } func (client *DdoscooClient) AssociateWebCertWithContext(ctx context.Context, request *aliddoscoo.AssociateWebCertRequest, runtime *dara.RuntimeOptions) (_result *aliddoscoo.AssociateWebCertResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } body := map[string]interface{}{} if !dara.IsNil(request.Cert) { body["Cert"] = request.Cert } if !dara.IsNil(request.CertId) { body["CertId"] = request.CertId } if !dara.IsNil(request.CertIdentifier) { body["CertIdentifier"] = request.CertIdentifier } if !dara.IsNil(request.CertName) { body["CertName"] = request.CertName } if !dara.IsNil(request.CertRegion) { body["CertRegion"] = request.CertRegion } if !dara.IsNil(request.Domain) { body["Domain"] = request.Domain } if !dara.IsNil(request.Key) { body["Key"] = request.Key } req := &openapiutil.OpenApiRequest{ Body: openapiutil.ParseToMap(body), } params := &openapiutil.Params{ Action: dara.String("AssociateWebCert"), Version: dara.String("2020-01-01"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &aliddoscoo.AssociateWebCertResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *DdoscooClient) DescribeDomainsWithContext(ctx context.Context, request *aliddoscoo.DescribeDomainsRequest, runtime *dara.RuntimeOptions) (_result *aliddoscoo.DescribeDomainsResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.InstanceIds) { query["InstanceIds"] = request.InstanceIds } if !dara.IsNil(request.ResourceGroupId) { query["ResourceGroupId"] = request.ResourceGroupId } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("DescribeDomains"), Version: dara.String("2020-01-01"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &aliddoscoo.DescribeDomainsResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } ================================================ FILE: pkg/core/deployer/providers/aliyun-esa/aliyun_esa.go ================================================ package aliyunesa import ( "context" "errors" "fmt" "log/slog" "strconv" "strings" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" aliesa "github.com/alibabacloud-go/esa-20240910/v2/client" "github.com/alibabacloud-go/tea/dara" "github.com/alibabacloud-go/tea/tea" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-esa/internal" ) type DeployerConfig struct { // 阿里云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 阿里云资源组 ID。 ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 阿里云 ESA 站点 ID。 SiteId int64 `json:"siteId"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.EsaClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, ResourceGroupId: config.ResourceGroupId, Region: lo. If(config.Region == "" || strings.HasPrefix(config.Region, "cn-"), "cn-hangzhou"). Else("ap-southeast-1"), }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { if d.config.SiteId == 0 { return nil, errors.New("config `siteId` is required") } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 配置站点证书 // REF: https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-setcertificate certId, _ := strconv.ParseInt(upres.CertId, 10, 64) setCertificateReq := &aliesa.SetCertificateRequest{ SiteId: tea.Int64(d.config.SiteId), Type: tea.String("cas"), CasId: tea.Int64(certId), Region: tea.String(d.config.Region), } setCertificateResp, err := d.sdkClient.SetCertificateWithContext(ctx, setCertificateReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'esa.SetCertificate'", slog.Any("request", setCertificateReq), slog.Any("response", setCertificateResp)) if err != nil { var sdkError *tea.SDKError if errors.As(err, &sdkError) { if tea.StringValue(sdkError.Code) == "Certificate.Duplicated" { return &deployer.DeployResult{}, nil } } return nil, fmt.Errorf("failed to execute sdk request 'esa.SetCertificate': %w", err) } return &deployer.DeployResult{}, nil } func createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.EsaClient, error) { // 接入点一览 https://api.aliyun.com/product/ESA var endpoint string switch region { case "": endpoint = "esa.cn-hangzhou.aliyuncs.com" default: endpoint = fmt.Sprintf("esa.%s.aliyuncs.com", region) } config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), Endpoint: tea.String(endpoint), } client, err := internal.NewEsaClient(config) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/aliyun-esa/aliyun_esa_test.go ================================================ package aliyunesa_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-esa" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fRegion string fSiteId int64 ) func init() { argsPrefix := "ALIYUNESA_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.Int64Var(&fSiteId, argsPrefix+"SITEID", 0, "") } /* Shell command to run this test: go test -v ./aliyun_esa_test.go -args \ --ALIYUNESA_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --ALIYUNESA_INPUTKEYPATH="/path/to/your-input-key.pem" \ --ALIYUNESA_ACCESSKEYID="your-access-key-id" \ --ALIYUNESA_ACCESSKEYSECRET="your-access-key-secret" \ --ALIYUNESA_REGION="cn-hangzhou" \ --ALIYUNESA_SITEID="your-esa-site-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("SITEID: %v", fSiteId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Region: fRegion, SiteId: fSiteId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/aliyun-esa/internal/client.go ================================================ package internal import ( "context" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" openapiutil "github.com/alibabacloud-go/darabonba-openapi/v2/utils" aliesa "github.com/alibabacloud-go/esa-20240910/v2/client" "github.com/alibabacloud-go/tea/dara" ) // This is a partial copy of https://github.com/alibabacloud-go/esa-20240910/blob/master/client/client_context_func.go // to lightweight the vendor packages in the built binary. type EsaClient struct { openapi.Client DisableSDKError *bool } func NewEsaClient(config *openapiutil.Config) (*EsaClient, error) { client := new(EsaClient) err := client.Init(config) return client, err } func (client *EsaClient) Init(config *openapiutil.Config) (_err error) { _err = client.Client.Init(config) if _err != nil { return _err } _err = client.CheckConfig(config) if _err != nil { return _err } return nil } func (client *EsaClient) SetCertificateWithContext(ctx context.Context, request *aliesa.SetCertificateRequest, runtime *dara.RuntimeOptions) (_result *aliesa.SetCertificateResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.KeyServerId) { query["KeyServerId"] = request.KeyServerId } if !dara.IsNil(request.OwnerId) { query["OwnerId"] = request.OwnerId } if !dara.IsNil(request.SecurityToken) { query["SecurityToken"] = request.SecurityToken } body := map[string]interface{}{} if !dara.IsNil(request.CasId) { body["CasId"] = request.CasId } if !dara.IsNil(request.Certificate) { body["Certificate"] = request.Certificate } if !dara.IsNil(request.Id) { body["Id"] = request.Id } if !dara.IsNil(request.Name) { body["Name"] = request.Name } if !dara.IsNil(request.PrivateKey) { body["PrivateKey"] = request.PrivateKey } if !dara.IsNil(request.Region) { body["Region"] = request.Region } if !dara.IsNil(request.SiteId) { body["SiteId"] = request.SiteId } if !dara.IsNil(request.Type) { body["Type"] = request.Type } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), Body: openapiutil.ParseToMap(body), } params := &openapiutil.Params{ Action: dara.String("SetCertificate"), Version: dara.String("2024-09-10"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &aliesa.SetCertificateResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } ================================================ FILE: pkg/core/deployer/providers/aliyun-esa-saas/aliyun_esasaas.go ================================================ package aliyunesasaas import ( "context" "errors" "fmt" "log/slog" "strconv" "strings" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" aliesa "github.com/alibabacloud-go/esa-20240910/v2/client" "github.com/alibabacloud-go/tea/dara" "github.com/alibabacloud-go/tea/tea" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-esa-saas/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 阿里云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 阿里云资源组 ID。 ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 阿里云 ESA 站点 ID。 SiteId int64 `json:"siteId"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // SaaS 域名(不支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.EsaClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, ResourceGroupId: config.ResourceGroupId, Region: lo. If(config.Region == "" || strings.HasPrefix(config.Region, "cn-"), "cn-hangzhou"). Else("ap-southeast-1"), }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { if d.config.SiteId == 0 { return nil, errors.New("config `siteId` is required") } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名 ID 列表 var hostnameIds []int64 switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } hostnameCandidates, err := d.getAllHostnames(ctx) if err != nil { return nil, err } hostname, ok := lo.Find(hostnameCandidates, func(hostname *aliesa.ListCustomHostnamesResponseBodyHostnames) bool { return d.config.Domain == tea.StringValue(hostname.Hostname) }) if !ok { return nil, fmt.Errorf("could not find hostname '%s'", d.config.Domain) } hostnameIds = []int64{tea.Int64Value(hostname.HostnameId)} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } hostnameCandidates, err := d.getAllHostnames(ctx) if err != nil { return nil, err } hostnames := lo.Filter(hostnameCandidates, func(hostname *aliesa.ListCustomHostnamesResponseBodyHostnames, _ int) bool { if strings.HasPrefix(d.config.Domain, "*.") { return xcerthostname.IsMatch(d.config.Domain, tea.StringValue(hostname.Hostname)) } else { return d.config.Domain == tea.StringValue(hostname.Hostname) } }) if len(hostnames) == 0 { return nil, errors.New("could not find any hostnames matched by wildcard") } hostnameIds = lo.Map(hostnames, func(hostname *aliesa.ListCustomHostnamesResponseBodyHostnames, _ int) int64 { return tea.Int64Value(hostname.HostnameId) }) } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } hostnameCandidates, err := d.getAllHostnames(ctx) if err != nil { return nil, err } hostnames := lo.Filter(hostnameCandidates, func(hostname *aliesa.ListCustomHostnamesResponseBodyHostnames, _ int) bool { return certX509.VerifyHostname(tea.StringValue(hostname.Hostname)) == nil }) if len(hostnames) == 0 { return nil, errors.New("could not find any hostnames matched by certificate") } hostnameIds = lo.Map(hostnames, func(hostname *aliesa.ListCustomHostnamesResponseBodyHostnames, _ int) int64 { return tea.Int64Value(hostname.HostnameId) }) } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(hostnameIds) == 0 { d.logger.Info("no esa saas hostnames to deploy") } else { d.logger.Info("found esa saas hostnames to deploy", slog.Any("hostnameIds", hostnameIds)) var errs []error certIdentifier := upres.ExtendedData["CertIdentifier"].(string) certIdentifierSeps := strings.SplitN(certIdentifier, "-", 2) if len(certIdentifierSeps) != 2 { return nil, fmt.Errorf("received invalid certificate identifier: '%s'", certIdentifier) } certId, _ := strconv.ParseInt(certIdentifierSeps[0], 10, 64) certRegion := certIdentifierSeps[1] for _, hostnameId := range hostnameIds { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateHostnameCertificate(ctx, hostnameId, certId, certRegion); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllHostnames(ctx context.Context) ([]*aliesa.ListCustomHostnamesResponseBodyHostnames, error) { hostnames := make([]*aliesa.ListCustomHostnamesResponseBodyHostnames, 0) // 查询 SaaS 域名列表 // REF: https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-getcustomhostname listCustomHostnamesPageNumber := 1 listCustomHostnamesPageSize := 100 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } listCustomHostnamesReq := &aliesa.ListCustomHostnamesRequest{ SiteId: tea.Int64(d.config.SiteId), PageNumber: tea.Int32(int32(listCustomHostnamesPageNumber)), PageSize: tea.Int32(int32(listCustomHostnamesPageSize)), } listCustomHostnamesResp, err := d.sdkClient.ListCustomHostnamesWithContext(ctx, listCustomHostnamesReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'esa.ListCustomHostnames'", slog.Any("request", listCustomHostnamesReq), slog.Any("response", listCustomHostnamesResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'esa.ListCustomHostnames': %w", err) } if listCustomHostnamesResp.Body == nil { break } ignoredStatuses := []string{"pending", "conflicted", "offline"} for _, hostnameItem := range listCustomHostnamesResp.Body.Hostnames { if lo.Contains(ignoredStatuses, tea.StringValue(hostnameItem.Status)) { continue } hostnames = append(hostnames, hostnameItem) } if len(listCustomHostnamesResp.Body.Hostnames) < listCustomHostnamesPageSize { break } listCustomHostnamesPageNumber++ } return hostnames, nil } func (d *Deployer) updateHostnameCertificate(ctx context.Context, cloudHostnameId int64, cloudCertId int64, cloudCertRegion string) error { // 更新 SaaS 域名 // REF: https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-updatecustomhostname updateCustomHostnameReq := &aliesa.UpdateCustomHostnameRequest{ HostnameId: tea.Int64(cloudHostnameId), SslFlag: tea.String("on"), CertType: tea.String("cas"), CasId: tea.Int64(cloudCertId), CasRegion: tea.String(cloudCertRegion), } updateCustomHostnameResp, err := d.sdkClient.UpdateCustomHostnameWithContext(ctx, updateCustomHostnameReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'esa.UpdateCustomHostname'", slog.Any("request", updateCustomHostnameReq), slog.Any("response", updateCustomHostnameResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'esa.UpdateCustomHostname': %w", err) } return nil } func createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.EsaClient, error) { // 接入点一览 https://api.aliyun.com/product/ESA var endpoint string switch region { case "": endpoint = "esa.cn-hangzhou.aliyuncs.com" default: endpoint = fmt.Sprintf("esa.%s.aliyuncs.com", region) } config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), Endpoint: tea.String(endpoint), } client, err := internal.NewEsaClient(config) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/aliyun-esa-saas/aliyun_esasaas_test.go ================================================ package aliyunesasaas_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-esa-saas" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fRegion string fSiteId int64 fDomain string ) func init() { argsPrefix := "ALIYUNESASAAS_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.Int64Var(&fSiteId, argsPrefix+"SITEID", 0, "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./aliyun_esasaas_test.go -args \ --ALIYUNESASAAS_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --ALIYUNESASAAS_INPUTKEYPATH="/path/to/your-input-key.pem" \ --ALIYUNESASAAS_ACCESSKEYID="your-access-key-id" \ --ALIYUNESASAAS_ACCESSKEYSECRET="your-access-key-secret" \ --ALIYUNESASAAS_REGION="cn-hangzhou" \ --ALIYUNESASAAS_SITEID="your-esa-site-id"\ --ALIYUNESASAAS_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("SITEID: %v", fSiteId), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Region: fRegion, SiteId: fSiteId, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/aliyun-esa-saas/consts.go ================================================ package aliyunesasaas const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/aliyun-esa-saas/internal/client.go ================================================ package internal import ( "context" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" openapiutil "github.com/alibabacloud-go/darabonba-openapi/v2/utils" aliesa "github.com/alibabacloud-go/esa-20240910/v2/client" "github.com/alibabacloud-go/tea/dara" ) // This is a partial copy of https://github.com/alibabacloud-go/esa-20240910/blob/master/client/client_context_func.go // to lightweight the vendor packages in the built binary. type EsaClient struct { openapi.Client DisableSDKError *bool } func NewEsaClient(config *openapiutil.Config) (*EsaClient, error) { client := new(EsaClient) err := client.Init(config) return client, err } func (client *EsaClient) Init(config *openapiutil.Config) (_err error) { _err = client.Client.Init(config) if _err != nil { return _err } _err = client.CheckConfig(config) if _err != nil { return _err } return nil } func (client *EsaClient) ListCustomHostnamesWithContext(ctx context.Context, request *aliesa.ListCustomHostnamesRequest, runtime *dara.RuntimeOptions) (_result *aliesa.ListCustomHostnamesResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.Hostname) { query["Hostname"] = request.Hostname } if !dara.IsNil(request.NameMatchType) { query["NameMatchType"] = request.NameMatchType } if !dara.IsNil(request.PageNumber) { query["PageNumber"] = request.PageNumber } if !dara.IsNil(request.PageSize) { query["PageSize"] = request.PageSize } if !dara.IsNil(request.RecordId) { query["RecordId"] = request.RecordId } if !dara.IsNil(request.SiteId) { query["SiteId"] = request.SiteId } if !dara.IsNil(request.Status) { query["Status"] = request.Status } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("ListCustomHostnames"), Version: dara.String("2024-09-10"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &aliesa.ListCustomHostnamesResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *EsaClient) UpdateCustomHostnameWithContext(ctx context.Context, request *aliesa.UpdateCustomHostnameRequest, runtime *dara.RuntimeOptions) (_result *aliesa.UpdateCustomHostnameResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.CasId) { query["CasId"] = request.CasId } if !dara.IsNil(request.CasRegion) { query["CasRegion"] = request.CasRegion } if !dara.IsNil(request.CertType) { query["CertType"] = request.CertType } if !dara.IsNil(request.Certificate) { query["Certificate"] = request.Certificate } if !dara.IsNil(request.HostnameId) { query["HostnameId"] = request.HostnameId } if !dara.IsNil(request.PrivateKey) { query["PrivateKey"] = request.PrivateKey } if !dara.IsNil(request.RecordId) { query["RecordId"] = request.RecordId } if !dara.IsNil(request.SslFlag) { query["SslFlag"] = request.SslFlag } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("UpdateCustomHostname"), Version: dara.String("2024-09-10"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &aliesa.UpdateCustomHostnameResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } ================================================ FILE: pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go ================================================ package aliyunfc import ( "context" "errors" "fmt" "log/slog" "strings" "time" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" alifc3 "github.com/alibabacloud-go/fc-20230330/v4/client" alifc2 "github.com/alibabacloud-go/fc-open-20210406/v2/client" "github.com/alibabacloud-go/tea/dara" "github.com/alibabacloud-go/tea/tea" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-fc/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 阿里云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 阿里云资源组 ID。 ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 服务版本。 // 可取值 "2.0"、"3.0"。 ServiceVersion string `json:"serviceVersion"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 自定义域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClients *wSDKClients } var _ deployer.Provider = (*Deployer)(nil) type wSDKClients struct { FC2 *internal.FcopenClient FC3 *internal.FcClient } func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } clients, err := createSDKClients(config.AccessKeyId, config.AccessKeySecret, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClients: clients, }, 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) (*deployer.DeployResult, error) { switch d.config.ServiceVersion { case "3", "3.0": if err := d.deployToFC3(ctx, certPEM, privkeyPEM); err != nil { return nil, err } case "2", "2.0": if err := d.deployToFC2(ctx, certPEM, privkeyPEM); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported service version '%s'", d.config.ServiceVersion) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToFC3(ctx context.Context, certPEM, privkeyPEM string) error { // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getFC3AllDomains(ctx) if err != nil { return err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return xcerthostname.IsMatch(d.config.Domain, domain) }) if len(domains) == 0 { return errors.New("could not find any domains matched by wildcard") } } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return err } domainCandidates, err := d.getFC3AllDomains(ctx) if err != nil { return err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return errors.New("could not find any domains matched by certificate") } } default: return fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no fc domains to deploy") } else { d.logger.Info("found fc domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateFC3DomainCertificate(ctx, domain, certPEM, privkeyPEM); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) deployToFC2(ctx context.Context, certPEM, privkeyPEM string) error { // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getFC2AllDomains(ctx) if err != nil { return err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return xcerthostname.IsMatch(d.config.Domain, domain) }) if len(domains) == 0 { return errors.New("could not find any domains matched by wildcard") } } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return err } domainCandidates, err := d.getFC2AllDomains(ctx) if err != nil { return err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return errors.New("could not find any domains matched by certificate") } } default: return fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no fc domains to deploy") } else { d.logger.Info("found fc domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateFC2DomainCertificate(ctx, domain, certPEM, privkeyPEM); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) getFC3AllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 列出自定义域名 // REF: https://help.aliyun.com/zh/functioncompute/fc/developer-reference/api-fc-2023-03-30-listcustomdomains listCustomDomainsNextToken := (*string)(nil) for { select { case <-ctx.Done(): return nil, ctx.Err() default: } listCustomDomainsReq := &alifc3.ListCustomDomainsRequest{ NextToken: listCustomDomainsNextToken, Limit: tea.Int32(100), } listCustomDomainsResp, err := d.sdkClients.FC3.ListCustomDomainsWithContext(ctx, listCustomDomainsReq, make(map[string]*string, 0), &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'fc.ListCustomDomains'", slog.Any("request", listCustomDomainsReq), slog.Any("response", listCustomDomainsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'fc.ListCustomDomains': %w", err) } if listCustomDomainsResp.Body == nil { break } for _, domainItem := range listCustomDomainsResp.Body.CustomDomains { domains = append(domains, tea.StringValue(domainItem.DomainName)) } if len(listCustomDomainsResp.Body.CustomDomains) == 0 || listCustomDomainsResp.Body.NextToken == nil { break } listCustomDomainsNextToken = listCustomDomainsResp.Body.NextToken } return domains, nil } func (d *Deployer) getFC2AllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 列出自定义域名 // REF: https://help.aliyun.com/zh/functioncompute/fc-2-0/developer-reference/api-fc-open-2021-04-06-listcustomdomains listCustomDomainsNextToken := (*string)(nil) for { select { case <-ctx.Done(): return nil, ctx.Err() default: } listCustomDomainsReq := &alifc2.ListCustomDomainsRequest{ NextToken: listCustomDomainsNextToken, Limit: tea.Int32(100), } listCustomDomainsResp, err := d.sdkClients.FC2.ListCustomDomains(listCustomDomainsReq) d.logger.Debug("sdk request 'fc.ListCustomDomains'", slog.Any("request", listCustomDomainsReq), slog.Any("response", listCustomDomainsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'fc.ListCustomDomains': %w", err) } if listCustomDomainsResp.Body == nil { break } for _, domainItem := range listCustomDomainsResp.Body.CustomDomains { domains = append(domains, tea.StringValue(domainItem.DomainName)) } if len(listCustomDomainsResp.Body.CustomDomains) == 0 || listCustomDomainsResp.Body.NextToken == nil { break } listCustomDomainsNextToken = listCustomDomainsResp.Body.NextToken } return domains, nil } func (d *Deployer) updateFC3DomainCertificate(ctx context.Context, domain string, certPEM, privkeyPEM string) error { // 获取自定义域名 // REF: https://help.aliyun.com/zh/functioncompute/fc-3-0/developer-reference/api-fc-2023-03-30-getcustomdomain getCustomDomainResp, err := d.sdkClients.FC3.GetCustomDomainWithContext(ctx, tea.String(domain), make(map[string]*string), &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'fc.GetCustomDomain'", slog.Any("response", getCustomDomainResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'fc.GetCustomDomain': %w", err) } else { if getCustomDomainResp.Body.CertConfig != nil && tea.StringValue(getCustomDomainResp.Body.CertConfig.Certificate) == certPEM { return nil } } // 更新自定义域名 // REF: https://help.aliyun.com/zh/functioncompute/fc-3-0/developer-reference/api-fc-2023-03-30-updatecustomdomain updateCustomDomainReq := &alifc3.UpdateCustomDomainRequest{ Body: &alifc3.UpdateCustomDomainInput{ CertConfig: &alifc3.CertConfig{ CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())), Certificate: tea.String(certPEM), PrivateKey: tea.String(privkeyPEM), }, Protocol: getCustomDomainResp.Body.Protocol, TlsConfig: getCustomDomainResp.Body.TlsConfig, }, } if tea.StringValue(updateCustomDomainReq.Body.Protocol) == "HTTP" { updateCustomDomainReq.Body.Protocol = tea.String("HTTP,HTTPS") } updateCustomDomainResp, err := d.sdkClients.FC3.UpdateCustomDomainWithContext(ctx, tea.String(domain), updateCustomDomainReq, make(map[string]*string), &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'fc.UpdateCustomDomain'", slog.Any("request", updateCustomDomainReq), slog.Any("response", updateCustomDomainResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'fc.UpdateCustomDomain': %w", err) } return nil } func (d *Deployer) updateFC2DomainCertificate(ctx context.Context, domain string, certPEM, privkeyPEM string) error { // 获取自定义域名 // REF: https://help.aliyun.com/zh/functioncompute/fc-2-0/developer-reference/api-fc-open-2021-04-06-getcustomdomain getCustomDomainResp, err := d.sdkClients.FC2.GetCustomDomain(tea.String(domain)) d.logger.Debug("sdk request 'fc.GetCustomDomain'", slog.Any("response", getCustomDomainResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'fc.GetCustomDomain': %w", err) } else { if getCustomDomainResp.Body.CertConfig != nil && tea.StringValue(getCustomDomainResp.Body.CertConfig.Certificate) == certPEM { return nil } } // 更新自定义域名 // REF: https://help.aliyun.com/zh/functioncompute/fc-2-0/developer-reference/api-fc-open-2021-04-06-updatecustomdomain updateCustomDomainReq := &alifc2.UpdateCustomDomainRequest{ CertConfig: &alifc2.CertConfig{ CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())), Certificate: tea.String(certPEM), PrivateKey: tea.String(privkeyPEM), }, Protocol: getCustomDomainResp.Body.Protocol, TlsConfig: getCustomDomainResp.Body.TlsConfig, } if tea.StringValue(updateCustomDomainReq.Protocol) == "HTTP" { updateCustomDomainReq.Protocol = tea.String("HTTP,HTTPS") } updateCustomDomainResp, err := d.sdkClients.FC2.UpdateCustomDomain(tea.String(domain), updateCustomDomainReq) d.logger.Debug("sdk request 'fc.UpdateCustomDomain'", slog.Any("request", updateCustomDomainReq), slog.Any("response", updateCustomDomainResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'fc.UpdateCustomDomain': %w", err) } return nil } func createSDKClients(accessKeyId, accessKeySecret, region string) (*wSDKClients, error) { // 接入点一览 https://api.aliyun.com/product/FC-Open var fc2Endpoint string switch region { case "": fc2Endpoint = "fc.aliyuncs.com" case "cn-hangzhou-finance": fc2Endpoint = fmt.Sprintf("%s.fc.aliyuncs.com", region) default: fc2Endpoint = fmt.Sprintf("fc.%s.aliyuncs.com", region) } fc2Config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), Endpoint: tea.String(fc2Endpoint), } fc2Client, err := internal.NewFcopenClient(fc2Config) if err != nil { return nil, err } // 接入点一览 https://api.aliyun.com/product/FC var fc3Endpoint string switch region { case "": fc3Endpoint = "fcv3.cn-hangzhou.aliyuncs.com" case "me-central-1", "cn-hangzhou-finance", "cn-shanghai-finance-1", "cn-heyuan-acdr-1": fc3Endpoint = fmt.Sprintf("%s.fc.aliyuncs.com", region) default: fc3Endpoint = fmt.Sprintf("fcv3.%s.aliyuncs.com", region) } fc3Config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), Endpoint: tea.String(fc3Endpoint), } fc3Client, err := internal.NewFcClient(fc3Config) if err != nil { return nil, err } return &wSDKClients{ FC2: fc2Client, FC3: fc3Client, }, nil } ================================================ FILE: pkg/core/deployer/providers/aliyun-fc/aliyun_fc_test.go ================================================ package aliyunfc_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-fc" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fRegion string fDomain string ) func init() { argsPrefix := "ALIYUNFC_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./aliyun_fc_test.go -args \ --ALIYUNFC_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --ALIYUNFC_INPUTKEYPATH="/path/to/your-input-key.pem" \ --ALIYUNFC_ACCESSKEYID="your-access-key-id" \ --ALIYUNFC_ACCESSKEYSECRET="your-access-key-secret" \ --ALIYUNFC_REGION="cn-hangzhou" \ --ALIYUNFC_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Region: fRegion, ServiceVersion: "3.0", DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/aliyun-fc/consts.go ================================================ package aliyunfc const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/aliyun-fc/internal/client.go ================================================ package internal import ( "context" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" openapiutilv2 "github.com/alibabacloud-go/darabonba-openapi/v2/utils" alifc "github.com/alibabacloud-go/fc-20230330/v4/client" alifcopen "github.com/alibabacloud-go/fc-open-20210406/v2/client" openapiutil "github.com/alibabacloud-go/openapi-util/service" util "github.com/alibabacloud-go/tea-utils/v2/service" "github.com/alibabacloud-go/tea/dara" "github.com/alibabacloud-go/tea/tea" ) // This is a partial copy of https://github.com/alibabacloud-go/fc-20230330/blob/master/client/client_context_func.go // to lightweight the vendor packages in the built binary. type FcClient struct { openapi.Client DisableSDKError *bool } func NewFcClient(config *openapiutilv2.Config) (*FcClient, error) { client := new(FcClient) err := client.Init(config) return client, err } func (client *FcClient) Init(config *openapiutilv2.Config) (_err error) { _err = client.Client.Init(config) if _err != nil { return _err } _err = client.CheckConfig(config) if _err != nil { return _err } return nil } func (client *FcClient) GetCustomDomainWithContext(ctx context.Context, domainName *string, headers map[string]*string, runtime *dara.RuntimeOptions) (_result *alifc.GetCustomDomainResponse, _err error) { req := &openapiutilv2.OpenApiRequest{ Headers: headers, } params := &openapiutilv2.Params{ Action: dara.String("GetCustomDomain"), Version: dara.String("2023-03-30"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/2023-03-30/custom-domains/" + dara.PercentEncode(dara.StringValue(domainName))), Method: dara.String("GET"), AuthType: dara.String("AK"), Style: dara.String("ROA"), ReqBodyType: dara.String("json"), BodyType: dara.String("json"), } _result = &alifc.GetCustomDomainResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *FcClient) ListCustomDomainsWithContext(ctx context.Context, request *alifc.ListCustomDomainsRequest, headers map[string]*string, runtime *dara.RuntimeOptions) (_result *alifc.ListCustomDomainsResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.Limit) { query["limit"] = request.Limit } if !dara.IsNil(request.NextToken) { query["nextToken"] = request.NextToken } if !dara.IsNil(request.Prefix) { query["prefix"] = request.Prefix } req := &openapiutilv2.OpenApiRequest{ Headers: headers, Query: openapiutil.Query(query), } params := &openapiutilv2.Params{ Action: dara.String("ListCustomDomains"), Version: dara.String("2023-03-30"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/2023-03-30/custom-domains"), Method: dara.String("GET"), AuthType: dara.String("AK"), Style: dara.String("ROA"), ReqBodyType: dara.String("json"), BodyType: dara.String("json"), } _result = &alifc.ListCustomDomainsResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *FcClient) UpdateCustomDomainWithContext(ctx context.Context, domainName *string, request *alifc.UpdateCustomDomainRequest, headers map[string]*string, runtime *dara.RuntimeOptions) (_result *alifc.UpdateCustomDomainResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } req := &openapiutilv2.OpenApiRequest{ Headers: headers, Body: openapiutil.ParseToMap(request.Body), } params := &openapiutilv2.Params{ Action: dara.String("UpdateCustomDomain"), Version: dara.String("2023-03-30"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/2023-03-30/custom-domains/" + dara.PercentEncode(dara.StringValue(domainName))), Method: dara.String("PUT"), AuthType: dara.String("AK"), Style: dara.String("ROA"), ReqBodyType: dara.String("json"), BodyType: dara.String("json"), } _result = &alifc.UpdateCustomDomainResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } // This is a partial copy of https://github.com/alibabacloud-go/fc-open-20210406/blob/master/client/client.go // to lightweight the vendor packages in the built binary. type FcopenClient struct { openapi.Client } func NewFcopenClient(config *openapi.Config) (*FcopenClient, error) { client := new(FcopenClient) err := client.Init(config) return client, err } func (client *FcopenClient) Init(config *openapi.Config) (_err error) { _err = client.Client.Init(config) if _err != nil { return _err } _err = client.CheckConfig(config) if _err != nil { return _err } return nil } func (client *FcopenClient) GetCustomDomain(domainName *string) (_result *alifcopen.GetCustomDomainResponse, _err error) { runtime := &util.RuntimeOptions{} headers := &alifcopen.GetCustomDomainHeaders{} _result = &alifcopen.GetCustomDomainResponse{} _body, _err := client.GetCustomDomainWithOptions(domainName, headers, runtime) if _err != nil { return _result, _err } _result = _body return _result, _err } func (client *FcopenClient) GetCustomDomainWithOptions(domainName *string, headers *alifcopen.GetCustomDomainHeaders, runtime *util.RuntimeOptions) (_result *alifcopen.GetCustomDomainResponse, _err error) { realHeaders := make(map[string]*string) if !tea.BoolValue(util.IsUnset(headers.CommonHeaders)) { realHeaders = headers.CommonHeaders } if !tea.BoolValue(util.IsUnset(headers.XFcAccountId)) { realHeaders["X-Fc-Account-Id"] = util.ToJSONString(headers.XFcAccountId) } if !tea.BoolValue(util.IsUnset(headers.XFcDate)) { realHeaders["X-Fc-Date"] = util.ToJSONString(headers.XFcDate) } if !tea.BoolValue(util.IsUnset(headers.XFcTraceId)) { realHeaders["X-Fc-Trace-Id"] = util.ToJSONString(headers.XFcTraceId) } req := &openapi.OpenApiRequest{ Headers: realHeaders, } params := &openapi.Params{ Action: tea.String("GetCustomDomain"), Version: tea.String("2021-04-06"), Protocol: tea.String("HTTPS"), Pathname: tea.String("/2021-04-06/custom-domains/" + tea.StringValue(openapiutil.GetEncodeParam(domainName))), Method: tea.String("GET"), AuthType: tea.String("AK"), Style: tea.String("ROA"), ReqBodyType: tea.String("json"), BodyType: tea.String("json"), } _result = &alifcopen.GetCustomDomainResponse{} _body, _err := client.CallApi(params, req, runtime) if _err != nil { return _result, _err } _err = tea.Convert(_body, &_result) return _result, _err } func (client *FcopenClient) ListCustomDomains(request *alifcopen.ListCustomDomainsRequest) (_result *alifcopen.ListCustomDomainsResponse, _err error) { runtime := &util.RuntimeOptions{} headers := &alifcopen.ListCustomDomainsHeaders{} _result = &alifcopen.ListCustomDomainsResponse{} _body, _err := client.ListCustomDomainsWithOptions(request, headers, runtime) if _err != nil { return _result, _err } _result = _body return _result, _err } func (client *FcopenClient) ListCustomDomainsWithOptions(request *alifcopen.ListCustomDomainsRequest, headers *alifcopen.ListCustomDomainsHeaders, runtime *util.RuntimeOptions) (_result *alifcopen.ListCustomDomainsResponse, _err error) { _err = util.ValidateModel(request) if _err != nil { return _result, _err } query := map[string]interface{}{} if !tea.BoolValue(util.IsUnset(request.Limit)) { query["limit"] = request.Limit } if !tea.BoolValue(util.IsUnset(request.NextToken)) { query["nextToken"] = request.NextToken } if !tea.BoolValue(util.IsUnset(request.Prefix)) { query["prefix"] = request.Prefix } if !tea.BoolValue(util.IsUnset(request.StartKey)) { query["startKey"] = request.StartKey } realHeaders := make(map[string]*string) if !tea.BoolValue(util.IsUnset(headers.CommonHeaders)) { realHeaders = headers.CommonHeaders } if !tea.BoolValue(util.IsUnset(headers.XFcAccountId)) { realHeaders["X-Fc-Account-Id"] = util.ToJSONString(headers.XFcAccountId) } if !tea.BoolValue(util.IsUnset(headers.XFcDate)) { realHeaders["X-Fc-Date"] = util.ToJSONString(headers.XFcDate) } if !tea.BoolValue(util.IsUnset(headers.XFcTraceId)) { realHeaders["X-Fc-Trace-Id"] = util.ToJSONString(headers.XFcTraceId) } req := &openapi.OpenApiRequest{ Headers: realHeaders, Query: openapiutil.Query(query), } params := &openapi.Params{ Action: tea.String("ListCustomDomains"), Version: tea.String("2021-04-06"), Protocol: tea.String("HTTPS"), Pathname: tea.String("/2021-04-06/custom-domains"), Method: tea.String("GET"), AuthType: tea.String("AK"), Style: tea.String("ROA"), ReqBodyType: tea.String("json"), BodyType: tea.String("json"), } _result = &alifcopen.ListCustomDomainsResponse{} _body, _err := client.CallApi(params, req, runtime) if _err != nil { return _result, _err } _err = tea.Convert(_body, &_result) return _result, _err } func (client *FcopenClient) UpdateCustomDomain(domainName *string, request *alifcopen.UpdateCustomDomainRequest) (_result *alifcopen.UpdateCustomDomainResponse, _err error) { runtime := &util.RuntimeOptions{} headers := &alifcopen.UpdateCustomDomainHeaders{} _result = &alifcopen.UpdateCustomDomainResponse{} _body, _err := client.UpdateCustomDomainWithOptions(domainName, request, headers, runtime) if _err != nil { return _result, _err } _result = _body return _result, _err } func (client *FcopenClient) UpdateCustomDomainWithOptions(domainName *string, request *alifcopen.UpdateCustomDomainRequest, headers *alifcopen.UpdateCustomDomainHeaders, runtime *util.RuntimeOptions) (_result *alifcopen.UpdateCustomDomainResponse, _err error) { _err = util.ValidateModel(request) if _err != nil { return _result, _err } body := map[string]interface{}{} if !tea.BoolValue(util.IsUnset(request.CertConfig)) { body["certConfig"] = request.CertConfig } if !tea.BoolValue(util.IsUnset(request.Protocol)) { body["protocol"] = request.Protocol } if !tea.BoolValue(util.IsUnset(request.RouteConfig)) { body["routeConfig"] = request.RouteConfig } if !tea.BoolValue(util.IsUnset(request.TlsConfig)) { body["tlsConfig"] = request.TlsConfig } if !tea.BoolValue(util.IsUnset(request.WafConfig)) { body["wafConfig"] = request.WafConfig } realHeaders := make(map[string]*string) if !tea.BoolValue(util.IsUnset(headers.CommonHeaders)) { realHeaders = headers.CommonHeaders } if !tea.BoolValue(util.IsUnset(headers.XFcAccountId)) { realHeaders["X-Fc-Account-Id"] = util.ToJSONString(headers.XFcAccountId) } if !tea.BoolValue(util.IsUnset(headers.XFcDate)) { realHeaders["X-Fc-Date"] = util.ToJSONString(headers.XFcDate) } if !tea.BoolValue(util.IsUnset(headers.XFcTraceId)) { realHeaders["X-Fc-Trace-Id"] = util.ToJSONString(headers.XFcTraceId) } req := &openapi.OpenApiRequest{ Headers: realHeaders, Body: openapiutil.ParseToMap(body), } params := &openapi.Params{ Action: tea.String("UpdateCustomDomain"), Version: tea.String("2021-04-06"), Protocol: tea.String("HTTPS"), Pathname: tea.String("/2021-04-06/custom-domains/" + tea.StringValue(openapiutil.GetEncodeParam(domainName))), Method: tea.String("PUT"), AuthType: tea.String("AK"), Style: tea.String("ROA"), ReqBodyType: tea.String("json"), BodyType: tea.String("json"), } _result = &alifcopen.UpdateCustomDomainResponse{} _body, _err := client.CallApi(params, req, runtime) if _err != nil { return _result, _err } _err = tea.Convert(_body, &_result) return _result, _err } ================================================ FILE: pkg/core/deployer/providers/aliyun-ga/aliyun_ga.go ================================================ package aliyunga import ( "context" "errors" "fmt" "log/slog" "strings" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" aliga "github.com/alibabacloud-go/ga-20191120/v4/client" "github.com/alibabacloud-go/tea/tea" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-ga/internal" ) type DeployerConfig struct { // 阿里云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 阿里云资源组 ID。 ResourceGroupId string `json:"resourceGroupId,omitempty"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 全球加速实例 ID。 AcceleratorId string `json:"acceleratorId"` // 全球加速监听 ID。 // 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。 ListenerId string `json:"listenerId,omitempty"` // SNI 域名(不支持泛域名)。 // 部署资源类型为 [RESOURCE_TYPE_ACCELERATOR]、[RESOURCE_TYPE_LISTENER] 时选填。 Domain string `json:"domain,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.GaClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, ResourceGroupId: config.ResourceGroupId, Region: "cn-hangzhou", }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_ACCELERATOR: if err := d.deployToAccelerator(ctx, upres.ExtendedData["CertIdentifier"].(string)); err != nil { return nil, err } case RESOURCE_TYPE_LISTENER: if err := d.deployToListener(ctx, upres.ExtendedData["CertIdentifier"].(string)); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToAccelerator(ctx context.Context, cloudCertId string) error { if d.config.AcceleratorId == "" { return errors.New("config `acceleratorId` is required") } // 查询 HTTPS 监听列表 // REF: https://help.aliyun.com/zh/ga/developer-reference/api-ga-2019-11-20-listlisteners listenerIds := make([]string, 0) listListenersPageNumber := 1 listListenersPageSize := 50 for { select { case <-ctx.Done(): return ctx.Err() default: } listListenersReq := &aliga.ListListenersRequest{ RegionId: tea.String("cn-hangzhou"), AcceleratorId: tea.String(d.config.AcceleratorId), PageNumber: tea.Int32(int32(listListenersPageNumber)), PageSize: tea.Int32(int32(listListenersPageSize)), } listListenersResp, err := d.sdkClient.ListListeners(listListenersReq) d.logger.Debug("sdk request 'ga.ListListeners'", slog.Any("request", listListenersReq), slog.Any("response", listListenersResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'ga.ListListeners': %w", err) } if listListenersResp.Body == nil { break } for _, listener := range listListenersResp.Body.Listeners { if strings.EqualFold(tea.StringValue(listener.Protocol), "https") { listenerIds = append(listenerIds, tea.StringValue(listener.ListenerId)) } } if len(listListenersResp.Body.Listeners) < listListenersPageSize { break } listListenersPageNumber++ } // 遍历更新监听证书 if len(listenerIds) == 0 { d.logger.Info("no ga listeners to deploy") } else { var errs []error d.logger.Info("found https listeners to deploy", slog.Any("listenerIds", listenerIds)) for _, listenerId := range listenerIds { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateListenerCertificate(ctx, d.config.AcceleratorId, listenerId, cloudCertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error { if d.config.AcceleratorId == "" { return errors.New("config `acceleratorId` is required") } if d.config.ListenerId == "" { return errors.New("config `listenerId` is required") } // 更新监听 if err := d.updateListenerCertificate(ctx, d.config.AcceleratorId, d.config.ListenerId, cloudCertId); err != nil { return err } return nil } func (d *Deployer) updateListenerCertificate(ctx context.Context, cloudAcceleratorId string, cloudListenerId string, cloudCertId string) error { // 查询监听绑定的证书列表 // REF: https://help.aliyun.com/zh/ga/developer-reference/api-ga-2019-11-20-listlistenercertificates listenerDefaultCertificate := (*aliga.ListListenerCertificatesResponseBodyCertificates)(nil) listenerAdditionalCertificates := make([]*aliga.ListListenerCertificatesResponseBodyCertificates, 0) listListenerCertificatesNextToken := (*string)(nil) for { listListenerCertificatesReq := &aliga.ListListenerCertificatesRequest{ RegionId: tea.String("cn-hangzhou"), AcceleratorId: tea.String(cloudAcceleratorId), ListenerId: tea.String(cloudListenerId), NextToken: listListenerCertificatesNextToken, MaxResults: tea.Int32(20), } listListenerCertificatesResp, err := d.sdkClient.ListListenerCertificates(listListenerCertificatesReq) d.logger.Debug("sdk request 'ga.ListListenerCertificates'", slog.Any("request", listListenerCertificatesReq), slog.Any("response", listListenerCertificatesResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'ga.ListListenerCertificates': %w", err) } if listListenerCertificatesResp.Body == nil { break } for _, certItem := range listListenerCertificatesResp.Body.Certificates { if tea.BoolValue(certItem.IsDefault) { listenerDefaultCertificate = certItem } else { listenerAdditionalCertificates = append(listenerAdditionalCertificates, certItem) } } if len(listListenerCertificatesResp.Body.Certificates) == 0 || listListenerCertificatesResp.Body.NextToken == nil { break } listListenerCertificatesNextToken = listListenerCertificatesResp.Body.NextToken } if d.config.Domain == "" { // 未指定 SNI,只需部署到监听器 if listenerDefaultCertificate != nil && tea.StringValue(listenerDefaultCertificate.CertificateId) == cloudCertId { d.logger.Info("no need to update ga listener default certificate") return nil } // 修改监听的属性 // REF: https://help.aliyun.com/zh/ga/developer-reference/api-ga-2019-11-20-updatelistener updateListenerReq := &aliga.UpdateListenerRequest{ RegionId: tea.String("cn-hangzhou"), ListenerId: tea.String(cloudListenerId), Certificates: []*aliga.UpdateListenerRequestCertificates{{ Id: tea.String(cloudCertId), }}, } updateListenerResp, err := d.sdkClient.UpdateListener(updateListenerReq) d.logger.Debug("sdk request 'ga.UpdateListener'", slog.Any("request", updateListenerReq), slog.Any("response", updateListenerResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'ga.UpdateListener': %w", err) } } else { // 指定 SNI,需部署到扩展域名 if lo.SomeBy(listenerAdditionalCertificates, func(item *aliga.ListListenerCertificatesResponseBodyCertificates) bool { return tea.StringValue(item.CertificateId) == cloudCertId }) { d.logger.Info("no need to update ga listener additional certificate") return nil } if lo.SomeBy(listenerAdditionalCertificates, func(item *aliga.ListListenerCertificatesResponseBodyCertificates) bool { return tea.StringValue(item.Domain) == d.config.Domain }) { // 为监听替换扩展证书 // REF: https://help.aliyun.com/zh/ga/developer-reference/api-ga-2019-11-20-updateadditionalcertificatewithlistener updateAdditionalCertificateWithListenerReq := &aliga.UpdateAdditionalCertificateWithListenerRequest{ RegionId: tea.String("cn-hangzhou"), AcceleratorId: tea.String(cloudAcceleratorId), ListenerId: tea.String(cloudListenerId), CertificateId: tea.String(cloudCertId), Domain: tea.String(d.config.Domain), } updateAdditionalCertificateWithListenerResp, err := d.sdkClient.UpdateAdditionalCertificateWithListener(updateAdditionalCertificateWithListenerReq) d.logger.Debug("sdk request 'ga.UpdateAdditionalCertificateWithListener'", slog.Any("request", updateAdditionalCertificateWithListenerReq), slog.Any("response", updateAdditionalCertificateWithListenerResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'ga.UpdateAdditionalCertificateWithListener': %w", err) } } else { // 为监听绑定扩展证书 // REF: https://help.aliyun.com/zh/ga/developer-reference/api-ga-2019-11-20-associateadditionalcertificateswithlistener associateAdditionalCertificatesWithListenerReq := &aliga.AssociateAdditionalCertificatesWithListenerRequest{ RegionId: tea.String("cn-hangzhou"), AcceleratorId: tea.String(cloudAcceleratorId), ListenerId: tea.String(cloudListenerId), Certificates: []*aliga.AssociateAdditionalCertificatesWithListenerRequestCertificates{{ Id: tea.String(cloudCertId), Domain: tea.String(d.config.Domain), }}, } associateAdditionalCertificatesWithListenerResp, err := d.sdkClient.AssociateAdditionalCertificatesWithListener(associateAdditionalCertificatesWithListenerReq) d.logger.Debug("sdk request 'ga.AssociateAdditionalCertificatesWithListener'", slog.Any("request", associateAdditionalCertificatesWithListenerReq), slog.Any("response", associateAdditionalCertificatesWithListenerResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'ga.AssociateAdditionalCertificatesWithListener': %w", err) } } } return nil } func createSDKClient(accessKeyId, accessKeySecret string) (*internal.GaClient, error) { // 接入点一览 https://api.aliyun.com/product/Ga config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), Endpoint: tea.String("ga.cn-hangzhou.aliyuncs.com"), } client, err := internal.NewGaClient(config) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/aliyun-ga/aliyun_ga_test.go ================================================ package aliyunga_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-ga" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fAcceleratorId string fListenerId string fDomain string ) func init() { argsPrefix := "ALIYUNGA_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fAcceleratorId, argsPrefix+"ACCELERATORID", "", "") flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./aliyun_ga_test.go -args \ --ALIYUNGA_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --ALIYUNGA_INPUTKEYPATH="/path/to/your-input-key.pem" \ --ALIYUNGA_ACCESSKEYID="your-access-key-id" \ --ALIYUNGA_ACCESSKEYSECRET="your-access-key-secret" \ --ALIYUNGA_ACCELERATORID="your-ga-accelerator-id" \ --ALIYUNGA_LISTENERID="your-ga-listener-id" \ --ALIYUNGA_DOMAIN="your-ga-sni-domain" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy_ToAccelerator", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("ACCELERATORID: %v", fAcceleratorId), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, ResourceType: provider.RESOURCE_TYPE_ACCELERATOR, AcceleratorId: fAcceleratorId, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) t.Run("Deploy_ToListener", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("LISTENERID: %v", fListenerId), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, ResourceType: provider.RESOURCE_TYPE_LISTENER, ListenerId: fListenerId, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/aliyun-ga/consts.go ================================================ package aliyunga const ( // 资源类型:部署到指定全球加速器。 RESOURCE_TYPE_ACCELERATOR = "accelerator" // 资源类型:部署到指定监听器。 RESOURCE_TYPE_LISTENER = "listener" ) ================================================ FILE: pkg/core/deployer/providers/aliyun-ga/internal/client.go ================================================ package internal import ( openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" aliga "github.com/alibabacloud-go/ga-20191120/v4/client" openapiutil "github.com/alibabacloud-go/openapi-util/service" util "github.com/alibabacloud-go/tea-utils/v2/service" "github.com/alibabacloud-go/tea/dara" "github.com/alibabacloud-go/tea/tea" ) // This is a partial copy of https://github.com/alibabacloud-go/ga-20191120/blob/master/client/client.go // to lightweight the vendor packages in the built binary. type GaClient struct { openapi.Client } func NewGaClient(config *openapi.Config) (*GaClient, error) { client := new(GaClient) err := client.Init(config) return client, err } func (client *GaClient) Init(config *openapi.Config) (_err error) { _err = client.Client.Init(config) if _err != nil { return _err } _err = client.CheckConfig(config) if _err != nil { return _err } return nil } func (client *GaClient) AssociateAdditionalCertificatesWithListenerWithOptions(request *aliga.AssociateAdditionalCertificatesWithListenerRequest, runtime *util.RuntimeOptions) (_result *aliga.AssociateAdditionalCertificatesWithListenerResponse, _err error) { _err = util.ValidateModel(request) if _err != nil { return _result, _err } query := map[string]interface{}{} if !tea.BoolValue(util.IsUnset(request.AcceleratorId)) { query["AcceleratorId"] = request.AcceleratorId } if !tea.BoolValue(util.IsUnset(request.Certificates)) { query["Certificates"] = request.Certificates } if !tea.BoolValue(util.IsUnset(request.ClientToken)) { query["ClientToken"] = request.ClientToken } if !tea.BoolValue(util.IsUnset(request.ListenerId)) { query["ListenerId"] = request.ListenerId } if !tea.BoolValue(util.IsUnset(request.RegionId)) { query["RegionId"] = request.RegionId } req := &openapi.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapi.Params{ Action: tea.String("AssociateAdditionalCertificatesWithListener"), Version: tea.String("2019-11-20"), Protocol: tea.String("HTTPS"), Pathname: tea.String("/"), Method: tea.String("POST"), AuthType: tea.String("AK"), Style: tea.String("RPC"), ReqBodyType: tea.String("formData"), BodyType: tea.String("json"), } _result = &aliga.AssociateAdditionalCertificatesWithListenerResponse{} _body, _err := client.CallApi(params, req, runtime) if _err != nil { return _result, _err } _err = tea.Convert(_body, &_result) return _result, _err } func (client *GaClient) AssociateAdditionalCertificatesWithListener(request *aliga.AssociateAdditionalCertificatesWithListenerRequest) (_result *aliga.AssociateAdditionalCertificatesWithListenerResponse, _err error) { runtime := &util.RuntimeOptions{} _result = &aliga.AssociateAdditionalCertificatesWithListenerResponse{} _body, _err := client.AssociateAdditionalCertificatesWithListenerWithOptions(request, runtime) if _err != nil { return _result, _err } _result = _body return _result, _err } func (client *GaClient) ListListenersWithOptions(request *aliga.ListListenersRequest, runtime *util.RuntimeOptions) (_result *aliga.ListListenersResponse, _err error) { _err = util.ValidateModel(request) if _err != nil { return _result, _err } query := map[string]interface{}{} if !tea.BoolValue(util.IsUnset(request.AcceleratorId)) { query["AcceleratorId"] = request.AcceleratorId } if !tea.BoolValue(util.IsUnset(request.PageNumber)) { query["PageNumber"] = request.PageNumber } if !tea.BoolValue(util.IsUnset(request.PageSize)) { query["PageSize"] = request.PageSize } if !dara.IsNil(request.Protocol) { query["Protocol"] = request.Protocol } if !tea.BoolValue(util.IsUnset(request.RegionId)) { query["RegionId"] = request.RegionId } req := &openapi.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapi.Params{ Action: tea.String("ListListeners"), Version: tea.String("2019-11-20"), Protocol: tea.String("HTTPS"), Pathname: tea.String("/"), Method: tea.String("POST"), AuthType: tea.String("AK"), Style: tea.String("RPC"), ReqBodyType: tea.String("formData"), BodyType: tea.String("json"), } _result = &aliga.ListListenersResponse{} _body, _err := client.CallApi(params, req, runtime) if _err != nil { return _result, _err } _err = tea.Convert(_body, &_result) return _result, _err } func (client *GaClient) ListListeners(request *aliga.ListListenersRequest) (_result *aliga.ListListenersResponse, _err error) { runtime := &util.RuntimeOptions{} _result = &aliga.ListListenersResponse{} _body, _err := client.ListListenersWithOptions(request, runtime) if _err != nil { return _result, _err } _result = _body return _result, _err } func (client *GaClient) ListListenerCertificatesWithOptions(request *aliga.ListListenerCertificatesRequest, runtime *util.RuntimeOptions) (_result *aliga.ListListenerCertificatesResponse, _err error) { _err = util.ValidateModel(request) if _err != nil { return _result, _err } query := map[string]interface{}{} if !tea.BoolValue(util.IsUnset(request.AcceleratorId)) { query["AcceleratorId"] = request.AcceleratorId } if !tea.BoolValue(util.IsUnset(request.ListenerId)) { query["ListenerId"] = request.ListenerId } if !tea.BoolValue(util.IsUnset(request.MaxResults)) { query["MaxResults"] = request.MaxResults } if !tea.BoolValue(util.IsUnset(request.NextToken)) { query["NextToken"] = request.NextToken } if !tea.BoolValue(util.IsUnset(request.RegionId)) { query["RegionId"] = request.RegionId } if !tea.BoolValue(util.IsUnset(request.Role)) { query["Role"] = request.Role } req := &openapi.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapi.Params{ Action: tea.String("ListListenerCertificates"), Version: tea.String("2019-11-20"), Protocol: tea.String("HTTPS"), Pathname: tea.String("/"), Method: tea.String("POST"), AuthType: tea.String("AK"), Style: tea.String("RPC"), ReqBodyType: tea.String("formData"), BodyType: tea.String("json"), } _result = &aliga.ListListenerCertificatesResponse{} _body, _err := client.CallApi(params, req, runtime) if _err != nil { return _result, _err } _err = tea.Convert(_body, &_result) return _result, _err } func (client *GaClient) ListListenerCertificates(request *aliga.ListListenerCertificatesRequest) (_result *aliga.ListListenerCertificatesResponse, _err error) { runtime := &util.RuntimeOptions{} _result = &aliga.ListListenerCertificatesResponse{} _body, _err := client.ListListenerCertificatesWithOptions(request, runtime) if _err != nil { return _result, _err } _result = _body return _result, _err } func (client *GaClient) UpdateAdditionalCertificateWithListenerWithOptions(request *aliga.UpdateAdditionalCertificateWithListenerRequest, runtime *util.RuntimeOptions) (_result *aliga.UpdateAdditionalCertificateWithListenerResponse, _err error) { _err = util.ValidateModel(request) if _err != nil { return _result, _err } query := map[string]interface{}{} if !tea.BoolValue(util.IsUnset(request.AcceleratorId)) { query["AcceleratorId"] = request.AcceleratorId } if !tea.BoolValue(util.IsUnset(request.CertificateId)) { query["CertificateId"] = request.CertificateId } if !tea.BoolValue(util.IsUnset(request.ClientToken)) { query["ClientToken"] = request.ClientToken } if !tea.BoolValue(util.IsUnset(request.Domain)) { query["Domain"] = request.Domain } if !tea.BoolValue(util.IsUnset(request.DryRun)) { query["DryRun"] = request.DryRun } if !tea.BoolValue(util.IsUnset(request.ListenerId)) { query["ListenerId"] = request.ListenerId } if !tea.BoolValue(util.IsUnset(request.RegionId)) { query["RegionId"] = request.RegionId } req := &openapi.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapi.Params{ Action: tea.String("UpdateAdditionalCertificateWithListener"), Version: tea.String("2019-11-20"), Protocol: tea.String("HTTPS"), Pathname: tea.String("/"), Method: tea.String("POST"), AuthType: tea.String("AK"), Style: tea.String("RPC"), ReqBodyType: tea.String("formData"), BodyType: tea.String("json"), } _result = &aliga.UpdateAdditionalCertificateWithListenerResponse{} _body, _err := client.CallApi(params, req, runtime) if _err != nil { return _result, _err } _err = tea.Convert(_body, &_result) return _result, _err } func (client *GaClient) UpdateAdditionalCertificateWithListener(request *aliga.UpdateAdditionalCertificateWithListenerRequest) (_result *aliga.UpdateAdditionalCertificateWithListenerResponse, _err error) { runtime := &util.RuntimeOptions{} _result = &aliga.UpdateAdditionalCertificateWithListenerResponse{} _body, _err := client.UpdateAdditionalCertificateWithListenerWithOptions(request, runtime) if _err != nil { return _result, _err } _result = _body return _result, _err } func (client *GaClient) UpdateListenerWithOptions(request *aliga.UpdateListenerRequest, runtime *util.RuntimeOptions) (_result *aliga.UpdateListenerResponse, _err error) { _err = util.ValidateModel(request) if _err != nil { return _result, _err } query := map[string]interface{}{} if !tea.BoolValue(util.IsUnset(request.BackendPorts)) { query["BackendPorts"] = request.BackendPorts } if !tea.BoolValue(util.IsUnset(request.Certificates)) { query["Certificates"] = request.Certificates } if !tea.BoolValue(util.IsUnset(request.ClientAffinity)) { query["ClientAffinity"] = request.ClientAffinity } if !tea.BoolValue(util.IsUnset(request.ClientToken)) { query["ClientToken"] = request.ClientToken } if !tea.BoolValue(util.IsUnset(request.Description)) { query["Description"] = request.Description } if !tea.BoolValue(util.IsUnset(request.HttpVersion)) { query["HttpVersion"] = request.HttpVersion } if !tea.BoolValue(util.IsUnset(request.IdleTimeout)) { query["IdleTimeout"] = request.IdleTimeout } if !tea.BoolValue(util.IsUnset(request.ListenerId)) { query["ListenerId"] = request.ListenerId } if !tea.BoolValue(util.IsUnset(request.Name)) { query["Name"] = request.Name } if !tea.BoolValue(util.IsUnset(request.PortRanges)) { query["PortRanges"] = request.PortRanges } if !tea.BoolValue(util.IsUnset(request.Protocol)) { query["Protocol"] = request.Protocol } if !tea.BoolValue(util.IsUnset(request.ProxyProtocol)) { query["ProxyProtocol"] = request.ProxyProtocol } if !tea.BoolValue(util.IsUnset(request.RegionId)) { query["RegionId"] = request.RegionId } if !tea.BoolValue(util.IsUnset(request.RequestTimeout)) { query["RequestTimeout"] = request.RequestTimeout } if !tea.BoolValue(util.IsUnset(request.SecurityPolicyId)) { query["SecurityPolicyId"] = request.SecurityPolicyId } if !tea.BoolValue(util.IsUnset(request.XForwardedForConfig)) { query["XForwardedForConfig"] = request.XForwardedForConfig } req := &openapi.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapi.Params{ Action: tea.String("UpdateListener"), Version: tea.String("2019-11-20"), Protocol: tea.String("HTTPS"), Pathname: tea.String("/"), Method: tea.String("POST"), AuthType: tea.String("AK"), Style: tea.String("RPC"), ReqBodyType: tea.String("formData"), BodyType: tea.String("json"), } _result = &aliga.UpdateListenerResponse{} _body, _err := client.CallApi(params, req, runtime) if _err != nil { return _result, _err } _err = tea.Convert(_body, &_result) return _result, _err } func (client *GaClient) UpdateListener(request *aliga.UpdateListenerRequest) (_result *aliga.UpdateListenerResponse, _err error) { runtime := &util.RuntimeOptions{} _result = &aliga.UpdateListenerResponse{} _body, _err := client.UpdateListenerWithOptions(request, runtime) if _err != nil { return _result, _err } _result = _body return _result, _err } ================================================ FILE: pkg/core/deployer/providers/aliyun-live/aliyun_live.go ================================================ package aliyunlive import ( "context" "errors" "fmt" "log/slog" "strings" "time" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" alilive "github.com/alibabacloud-go/live-20161101/v2/client" "github.com/alibabacloud-go/tea/dara" "github.com/alibabacloud-go/tea/tea" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-live/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 阿里云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 阿里云资源组 ID。 ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 直播流域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.LiveClient } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region) 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) (*deployer.DeployResult, error) { // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } // "*.example.com" → ".example.com",适配阿里云 Live 要求的泛域名格式 domain := strings.TrimPrefix(d.config.Domain, "*") domains = []string{domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return xcerthostname.IsMatch(d.config.Domain, domain) || strings.TrimPrefix(d.config.Domain, "*") == strings.TrimPrefix(domain, "*") }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by wildcard") } } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil || strings.TrimPrefix(d.config.Domain, "*") == strings.TrimPrefix(domain, "*") }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no live domains to deploy") } else { d.logger.Info("found live domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, certPEM, privkeyPEM); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 查询用户名下所有的直播域名 // REF: https://help.aliyun.com/zh/live/developer-reference/api-live-2016-11-01-describeliveuserdomains describeUserLiveDomainsPageNumber := 1 describeUserLiveDomainsPageSize := 50 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } describeUserLiveDomainsReq := &alilive.DescribeLiveUserDomainsRequest{ ResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId), RegionName: tea.String(d.config.Region), DomainStatus: tea.String("online"), PageNumber: tea.Int32(int32(describeUserLiveDomainsPageNumber)), PageSize: tea.Int32(int32(describeUserLiveDomainsPageSize)), } describeUserLiveDomainsResp, err := d.sdkClient.DescribeLiveUserDomainsWithContext(ctx, describeUserLiveDomainsReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'live.DescribeLiveUserDomains'", slog.Any("request", describeUserLiveDomainsReq), slog.Any("response", describeUserLiveDomainsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'live.DescribeLiveUserDomains': %w", err) } if describeUserLiveDomainsResp.Body == nil || describeUserLiveDomainsResp.Body.Domains == nil { break } for _, domainItem := range describeUserLiveDomainsResp.Body.Domains.PageData { domains = append(domains, tea.StringValue(domainItem.DomainName)) } if len(describeUserLiveDomainsResp.Body.Domains.PageData) < describeUserLiveDomainsPageSize { break } describeUserLiveDomainsPageNumber++ } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, certPEM, privkeyPEM string) error { // 设置域名证书 // REF: https://help.aliyun.com/zh/live/developer-reference/api-live-2016-11-01-setlivedomaincertificate setLiveDomainSSLCertificateReq := &alilive.SetLiveDomainCertificateRequest{ DomainName: tea.String(domain), CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())), CertType: tea.String("upload"), SSLProtocol: tea.String("on"), SSLPub: tea.String(certPEM), SSLPri: tea.String(privkeyPEM), } setLiveDomainSSLCertificateResp, err := d.sdkClient.SetLiveDomainCertificateWithContext(ctx, setLiveDomainSSLCertificateReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'live.SetLiveDomainCertificate'", slog.Any("request", setLiveDomainSSLCertificateReq), slog.Any("response", setLiveDomainSSLCertificateResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'live.SetLiveDomainCertificate': %w", err) } return nil } func createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.LiveClient, error) { // 接入点一览 https://api.aliyun.com/product/live var endpoint string switch region { case "", "cn-qingdao", "cn-beijing", "cn-shanghai", "cn-shenzhen", "ap-northeast-1", "ap-southeast-5", "me-central-1": endpoint = "live.aliyuncs.com" default: endpoint = fmt.Sprintf("live.%s.aliyuncs.com", region) } config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), Endpoint: tea.String(endpoint), } client, err := internal.NewLiveClient(config) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/aliyun-live/aliyun_live_test.go ================================================ package aliyunlive_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-live" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fRegion string fDomain string ) func init() { argsPrefix := "ALIYUNLIVE_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./aliyun_live_test.go -args \ --ALIYUNLIVE_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --ALIYUNLIVE_INPUTKEYPATH="/path/to/your-input-key.pem" \ --ALIYUNLIVE_ACCESSKEYID="your-access-key-id" \ --ALIYUNLIVE_ACCESSKEYSECRET="your-access-key-secret" \ --ALIYUNLIVE_REGION="cn-hangzhou" \ --ALIYUNLIVE_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Region: fRegion, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/aliyun-live/consts.go ================================================ package aliyunlive const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/aliyun-live/internal/client.go ================================================ package internal import ( "context" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" openapiutil "github.com/alibabacloud-go/darabonba-openapi/v2/utils" alilive "github.com/alibabacloud-go/live-20161101/v2/client" "github.com/alibabacloud-go/tea/dara" ) // This is a partial copy of https://github.com/alibabacloud-go/live-20161101/blob/master/client/client_context_func.go // to lightweight the vendor packages in the built binary. type LiveClient struct { openapi.Client DisableSDKError *bool } func NewLiveClient(config *openapiutil.Config) (*LiveClient, error) { client := new(LiveClient) err := client.Init(config) return client, err } func (client *LiveClient) Init(config *openapiutil.Config) (_err error) { _err = client.Client.Init(config) if _err != nil { return _err } _err = client.CheckConfig(config) if _err != nil { return _err } return nil } func (client *LiveClient) DescribeLiveUserDomainsWithContext(ctx context.Context, request *alilive.DescribeLiveUserDomainsRequest, runtime *dara.RuntimeOptions) (_result *alilive.DescribeLiveUserDomainsResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.DomainName) { query["DomainName"] = request.DomainName } if !dara.IsNil(request.DomainSearchType) { query["DomainSearchType"] = request.DomainSearchType } if !dara.IsNil(request.DomainStatus) { query["DomainStatus"] = request.DomainStatus } if !dara.IsNil(request.LiveDomainType) { query["LiveDomainType"] = request.LiveDomainType } if !dara.IsNil(request.OwnerId) { query["OwnerId"] = request.OwnerId } if !dara.IsNil(request.PageNumber) { query["PageNumber"] = request.PageNumber } if !dara.IsNil(request.PageSize) { query["PageSize"] = request.PageSize } if !dara.IsNil(request.RegionName) { query["RegionName"] = request.RegionName } if !dara.IsNil(request.ResourceGroupId) { query["ResourceGroupId"] = request.ResourceGroupId } if !dara.IsNil(request.SecurityToken) { query["SecurityToken"] = request.SecurityToken } if !dara.IsNil(request.Tag) { query["Tag"] = request.Tag } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("DescribeLiveUserDomains"), Version: dara.String("2016-11-01"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alilive.DescribeLiveUserDomainsResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *LiveClient) SetLiveDomainCertificateWithContext(ctx context.Context, request *alilive.SetLiveDomainCertificateRequest, runtime *dara.RuntimeOptions) (_result *alilive.SetLiveDomainCertificateResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.CertName) { query["CertName"] = request.CertName } if !dara.IsNil(request.CertType) { query["CertType"] = request.CertType } if !dara.IsNil(request.DomainName) { query["DomainName"] = request.DomainName } if !dara.IsNil(request.ForceSet) { query["ForceSet"] = request.ForceSet } if !dara.IsNil(request.OwnerId) { query["OwnerId"] = request.OwnerId } if !dara.IsNil(request.SSLPri) { query["SSLPri"] = request.SSLPri } if !dara.IsNil(request.SSLProtocol) { query["SSLProtocol"] = request.SSLProtocol } if !dara.IsNil(request.SSLPub) { query["SSLPub"] = request.SSLPub } if !dara.IsNil(request.SecurityToken) { query["SecurityToken"] = request.SecurityToken } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("SetLiveDomainCertificate"), Version: dara.String("2016-11-01"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alilive.SetLiveDomainCertificateResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } ================================================ FILE: pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go ================================================ package aliyunnlb import ( "context" "errors" "fmt" "log/slog" "strings" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" alinlb "github.com/alibabacloud-go/nlb-20220430/v4/client" "github.com/alibabacloud-go/tea/dara" "github.com/alibabacloud-go/tea/tea" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-nlb/internal" ) type DeployerConfig struct { // 阿里云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 阿里云资源组 ID。 ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 负载均衡实例 ID。 // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER] 时必填。 LoadbalancerId string `json:"loadbalancerId,omitempty"` // 负载均衡监听 ID。 // 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。 ListenerId string `json:"listenerId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.NlbClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, ResourceGroupId: config.ResourceGroupId, Region: lo. If(config.Region == "" || strings.HasPrefix(config.Region, "cn-"), "cn-hangzhou"). Else("ap-southeast-1"), }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_LOADBALANCER: if err := d.deployToLoadbalancer(ctx, upres.ExtendedData["CertIdentifier"].(string)); err != nil { return nil, err } case RESOURCE_TYPE_LISTENER: if err := d.deployToListener(ctx, upres.ExtendedData["CertIdentifier"].(string)); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error { if d.config.LoadbalancerId == "" { return errors.New("config `loadbalancerId` is required") } // 查询负载均衡实例的详细信息 // REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getloadbalancerattribute getLoadBalancerAttributeReq := &alinlb.GetLoadBalancerAttributeRequest{ LoadBalancerId: tea.String(d.config.LoadbalancerId), } getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttributeWithContext(ctx, getLoadBalancerAttributeReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'nlb.GetLoadBalancerAttribute'", slog.Any("request", getLoadBalancerAttributeReq), slog.Any("response", getLoadBalancerAttributeResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'nlb.GetLoadBalancerAttribute': %w", err) } // 查询 TCPSSL 监听列表 // REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-listlisteners listenerIds := make([]string, 0) listListenersToken := (*string)(nil) for { select { case <-ctx.Done(): return ctx.Err() default: } listListenersReq := &alinlb.ListListenersRequest{ NextToken: listListenersToken, MaxResults: tea.Int32(100), LoadBalancerIds: tea.StringSlice([]string{d.config.LoadbalancerId}), ListenerProtocol: tea.String("TCPSSL"), } listListenersResp, err := d.sdkClient.ListListenersWithContext(ctx, listListenersReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'nlb.ListListeners'", slog.Any("request", listListenersReq), slog.Any("response", listListenersResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'nlb.ListListeners': %w", err) } if listListenersResp.Body == nil { break } for _, listener := range listListenersResp.Body.Listeners { listenerIds = append(listenerIds, tea.StringValue(listener.ListenerId)) } if len(listListenersResp.Body.Listeners) == 0 || listListenersResp.Body.NextToken == nil { break } listListenersToken = listListenersResp.Body.NextToken } // 遍历更新监听证书 if len(listenerIds) == 0 { d.logger.Info("no nlb listeners to deploy") } else { d.logger.Info("found tcpssl listeners to deploy", slog.Any("listenerIds", listenerIds)) var errs []error for _, listenerId := range listenerIds { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error { if d.config.ListenerId == "" { return errors.New("config `listenerId` is required") } // 更新监听 if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil { return err } return nil } func (d *Deployer) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error { // 查询监听的属性 // REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getlistenerattribute getListenerAttributeReq := &alinlb.GetListenerAttributeRequest{ ListenerId: tea.String(cloudListenerId), } getListenerAttributeResp, err := d.sdkClient.GetListenerAttributeWithContext(ctx, getListenerAttributeReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'nlb.GetListenerAttribute'", slog.Any("request", getListenerAttributeReq), slog.Any("response", getListenerAttributeResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'nlb.GetListenerAttribute': %w", err) } // 修改监听的属性 // REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-updatelistenerattribute updateListenerAttributeReq := &alinlb.UpdateListenerAttributeRequest{ ListenerId: tea.String(cloudListenerId), CertificateIds: []*string{tea.String(cloudCertId)}, } updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttributeWithContext(ctx, updateListenerAttributeReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'nlb.UpdateListenerAttribute'", slog.Any("request", updateListenerAttributeReq), slog.Any("response", updateListenerAttributeResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'nlb.UpdateListenerAttribute': %w", err) } return nil } func createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.NlbClient, error) { // 接入点一览 https://api.aliyun.com/product/Nlb var endpoint string switch region { case "": endpoint = "nlb.cn-hangzhou.aliyuncs.com" default: endpoint = fmt.Sprintf("nlb.%s.aliyuncs.com", region) } config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), Endpoint: tea.String(endpoint), } client, err := internal.NewNlbClient(config) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb_test.go ================================================ package aliyunnlb_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-nlb" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fRegion string fLoadbalancerId string fListenerId string ) func init() { argsPrefix := "ALIYUNNLB_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "") flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "") } /* Shell command to run this test: go test -v ./aliyun_nlb_test.go -args \ --ALIYUNNLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --ALIYUNNLB_INPUTKEYPATH="/path/to/your-input-key.pem" \ --ALIYUNNLB_ACCESSKEYID="your-access-key-id" \ --ALIYUNNLB_ACCESSKEYSECRET="your-access-key-secret" \ --ALIYUNNLB_REGION="cn-hangzhou" \ --ALIYUNNLB_LOADBALANCERID="your-nlb-instance-id" \ --ALIYUNNLB_LISTENERID="your-nlb-listener-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy_ToLoadbalancer", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Region: fRegion, ResourceType: provider.RESOURCE_TYPE_LOADBALANCER, LoadbalancerId: fLoadbalancerId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) t.Run("Deploy_ToListener", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId), fmt.Sprintf("LISTENERID: %v", fListenerId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Region: fRegion, ResourceType: provider.RESOURCE_TYPE_LISTENER, ListenerId: fListenerId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/aliyun-nlb/consts.go ================================================ package aliyunnlb const ( // 资源类型:部署到指定负载均衡器。 RESOURCE_TYPE_LOADBALANCER = "loadbalancer" // 资源类型:部署到指定监听器。 RESOURCE_TYPE_LISTENER = "listener" ) ================================================ FILE: pkg/core/deployer/providers/aliyun-nlb/internal/client.go ================================================ package internal import ( "context" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" openapiutil "github.com/alibabacloud-go/darabonba-openapi/v2/utils" alinlb "github.com/alibabacloud-go/nlb-20220430/v4/client" "github.com/alibabacloud-go/tea/dara" ) // This is a partial copy of https://github.com/alibabacloud-go/nlb-20220430/blob/master/client/client_context_func.go // to lightweight the vendor packages in the built binary. type NlbClient struct { openapi.Client DisableSDKError *bool } func NewNlbClient(config *openapiutil.Config) (*NlbClient, error) { client := new(NlbClient) err := client.Init(config) return client, err } func (client *NlbClient) Init(config *openapiutil.Config) (_err error) { _err = client.Client.Init(config) if _err != nil { return _err } _err = client.CheckConfig(config) if _err != nil { return _err } return nil } func (client *NlbClient) GetListenerAttributeWithContext(ctx context.Context, request *alinlb.GetListenerAttributeRequest, runtime *dara.RuntimeOptions) (_result *alinlb.GetListenerAttributeResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.ClientToken) { query["ClientToken"] = request.ClientToken } if !dara.IsNil(request.DryRun) { query["DryRun"] = request.DryRun } if !dara.IsNil(request.ListenerId) { query["ListenerId"] = request.ListenerId } if !dara.IsNil(request.RegionId) { query["RegionId"] = request.RegionId } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("GetListenerAttribute"), Version: dara.String("2022-04-30"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alinlb.GetListenerAttributeResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *NlbClient) GetLoadBalancerAttributeWithContext(ctx context.Context, request *alinlb.GetLoadBalancerAttributeRequest, runtime *dara.RuntimeOptions) (_result *alinlb.GetLoadBalancerAttributeResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.ClientToken) { query["ClientToken"] = request.ClientToken } if !dara.IsNil(request.DryRun) { query["DryRun"] = request.DryRun } if !dara.IsNil(request.LoadBalancerId) { query["LoadBalancerId"] = request.LoadBalancerId } if !dara.IsNil(request.RegionId) { query["RegionId"] = request.RegionId } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("GetLoadBalancerAttribute"), Version: dara.String("2022-04-30"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alinlb.GetLoadBalancerAttributeResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *NlbClient) ListListenersWithContext(ctx context.Context, request *alinlb.ListListenersRequest, runtime *dara.RuntimeOptions) (_result *alinlb.ListListenersResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.ListenerIds) { query["ListenerIds"] = request.ListenerIds } if !dara.IsNil(request.ListenerProtocol) { query["ListenerProtocol"] = request.ListenerProtocol } if !dara.IsNil(request.LoadBalancerIds) { query["LoadBalancerIds"] = request.LoadBalancerIds } if !dara.IsNil(request.MaxResults) { query["MaxResults"] = request.MaxResults } if !dara.IsNil(request.NextToken) { query["NextToken"] = request.NextToken } if !dara.IsNil(request.RegionId) { query["RegionId"] = request.RegionId } if !dara.IsNil(request.SecSensorEnabled) { query["SecSensorEnabled"] = request.SecSensorEnabled } if !dara.IsNil(request.Tag) { query["Tag"] = request.Tag } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("ListListeners"), Version: dara.String("2022-04-30"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alinlb.ListListenersResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *NlbClient) UpdateListenerAttributeWithContext(ctx context.Context, tmpReq *alinlb.UpdateListenerAttributeRequest, runtime *dara.RuntimeOptions) (_result *alinlb.UpdateListenerAttributeResponse, _err error) { _err = tmpReq.Validate() if _err != nil { return _result, _err } request := &alinlb.UpdateListenerAttributeShrinkRequest{} openapiutil.Convert(tmpReq, request) if !dara.IsNil(tmpReq.ProxyProtocolV2Config) { request.ProxyProtocolV2ConfigShrink = openapiutil.ArrayToStringWithSpecifiedStyle(tmpReq.ProxyProtocolV2Config, dara.String("ProxyProtocolV2Config"), dara.String("json")) } body := map[string]interface{}{} if !dara.IsNil(request.AlpnEnabled) { body["AlpnEnabled"] = request.AlpnEnabled } if !dara.IsNil(request.AlpnPolicy) { body["AlpnPolicy"] = request.AlpnPolicy } if !dara.IsNil(request.CaCertificateIds) { body["CaCertificateIds"] = request.CaCertificateIds } if !dara.IsNil(request.CaEnabled) { body["CaEnabled"] = request.CaEnabled } if !dara.IsNil(request.CertificateIds) { body["CertificateIds"] = request.CertificateIds } if !dara.IsNil(request.ClientToken) { body["ClientToken"] = request.ClientToken } if !dara.IsNil(request.Cps) { body["Cps"] = request.Cps } if !dara.IsNil(request.DryRun) { body["DryRun"] = request.DryRun } if !dara.IsNil(request.IdleTimeout) { body["IdleTimeout"] = request.IdleTimeout } if !dara.IsNil(request.ListenerDescription) { body["ListenerDescription"] = request.ListenerDescription } if !dara.IsNil(request.ListenerId) { body["ListenerId"] = request.ListenerId } if !dara.IsNil(request.Mss) { body["Mss"] = request.Mss } if !dara.IsNil(request.ProxyProtocolEnabled) { body["ProxyProtocolEnabled"] = request.ProxyProtocolEnabled } if !dara.IsNil(request.ProxyProtocolV2ConfigShrink) { body["ProxyProtocolV2Config"] = request.ProxyProtocolV2ConfigShrink } if !dara.IsNil(request.RegionId) { body["RegionId"] = request.RegionId } if !dara.IsNil(request.SecSensorEnabled) { body["SecSensorEnabled"] = request.SecSensorEnabled } if !dara.IsNil(request.SecurityPolicyId) { body["SecurityPolicyId"] = request.SecurityPolicyId } if !dara.IsNil(request.ServerGroupId) { body["ServerGroupId"] = request.ServerGroupId } req := &openapiutil.OpenApiRequest{ Body: openapiutil.ParseToMap(body), } params := &openapiutil.Params{ Action: dara.String("UpdateListenerAttribute"), Version: dara.String("2022-04-30"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alinlb.UpdateListenerAttributeResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } ================================================ FILE: pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go ================================================ package aliyunoss import ( "context" "errors" "fmt" "log/slog" "github.com/alibabacloud-go/tea/tea" "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss" "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials" "github.com/certimate-go/certimate/pkg/core/deployer" ) type DeployerConfig struct { // 阿里云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 阿里云资源组 ID。 ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 存储桶名。 Bucket string `json:"bucket"` // 自定义域名(不支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *oss.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region) 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) (*deployer.DeployResult, error) { if d.config.Bucket == "" { return nil, errors.New("config `bucket` is required") } if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } // 为存储空间绑定自定义域名 // REF: https://help.aliyun.com/zh/oss/developer-reference/putcname putCnameReq := &oss.PutCnameRequest{ Bucket: tea.String(d.config.Bucket), BucketCnameConfiguration: &oss.BucketCnameConfiguration{ Domain: tea.String(d.config.Domain), CertificateConfiguration: &oss.CertificateConfiguration{ Certificate: tea.String(certPEM), PrivateKey: tea.String(privkeyPEM), Force: tea.Bool(true), }, }, } putCnameResp, err := d.sdkClient.PutCname(ctx, putCnameReq) d.logger.Debug("sdk request 'oss.PutCname'", slog.Any("request", putCnameReq), slog.Any("response", putCnameResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'oss.PutCname': %w", err) } return &deployer.DeployResult{}, nil } func createSDKClient(accessKeyId, accessKeySecret, region string) (*oss.Client, error) { // 接入点一览 https://api.aliyun.com/product/Oss var endpoint string switch region { case "": endpoint = "oss.aliyuncs.com" case "cn-hzjbp", "cn-hzjbp-a", "cn-hzjbp-b": endpoint = "oss-cn-hzjbp-a-internal.aliyuncs.com" case "cn-shanghai-finance-1", "cn-shenzhen-finance-1", "cn-beijing-finance-1", "cn-north-2-gov-1": endpoint = fmt.Sprintf("oss-%s-internal.aliyuncs.com", region) default: endpoint = fmt.Sprintf("oss-%s.aliyuncs.com", region) } provider := credentials.NewStaticCredentialsProvider(accessKeyId, accessKeySecret) config := oss.LoadDefaultConfig(). WithCredentialsProvider(provider). WithEndpoint(endpoint) if region != "" { config = config.WithRegion(region) } client := oss.NewClient(config) return client, nil } ================================================ FILE: pkg/core/deployer/providers/aliyun-oss/aliyun_oss_test.go ================================================ package aliyunoss_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-oss" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fRegion string fBucket string fDomain string ) func init() { argsPrefix := "ALIYUNOSS_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fBucket, argsPrefix+"BUCKET", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./aliyun_oss_test.go -args \ --ALIYUNOSS_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --ALIYUNOSS_INPUTKEYPATH="/path/to/your-input-key.pem" \ --ALIYUNOSS_ACCESSKEYID="your-access-key-id" \ --ALIYUNOSS_ACCESSKEYSECRET="your-access-key-secret" \ --ALIYUNOSS_REGION="cn-hangzhou" \ --ALIYUNOSS_BUCKET="your-oss-bucket" \ --ALIYUNOSS_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("BUCKET: %v", fBucket), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Region: fRegion, Bucket: fBucket, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/aliyun-vod/aliyun_vod.go ================================================ package aliyunvod import ( "context" "errors" "fmt" "log/slog" "strconv" "strings" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" "github.com/alibabacloud-go/tea/dara" "github.com/alibabacloud-go/tea/tea" alivod "github.com/alibabacloud-go/vod-20170321/v4/client" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-vod/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 阿里云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 阿里云资源组 ID。 ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 点播加速域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.VodClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, ResourceGroupId: config.ResourceGroupId, Region: lo. If(config.Region == "" || strings.HasPrefix(config.Region, "cn-"), "cn-hangzhou"). Else("ap-southeast-1"), }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, 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) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return xcerthostname.IsMatch(d.config.Domain, domain) }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by wildcard") } } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no vod domains to deploy") } else { d.logger.Info("found vod domains to deploy", slog.Any("domains", domains)) var errs []error certIdentifier := upres.ExtendedData["CertIdentifier"].(string) certIdentifierSeps := strings.SplitN(certIdentifier, "-", 2) if len(certIdentifierSeps) != 2 { return nil, fmt.Errorf("received invalid certificate identifier: '%s'", certIdentifier) } certId, _ := strconv.ParseInt(certIdentifierSeps[0], 10, 64) certName := upres.CertName certRegion := certIdentifierSeps[1] for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, certId, certName, certRegion); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 查询加速域名列表 // REF: https://help.aliyun.com/zh/live/developer-reference/api-live-2016-11-01-describeliveuserdomains describeVodUserDomainsPageNumber := 1 describeVodUserDomainsPageSize := 50 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } describeVodUserDomainsReq := &alivod.DescribeVodUserDomainsRequest{ DomainStatus: tea.String("online"), PageNumber: tea.Int32(int32(describeVodUserDomainsPageNumber)), PageSize: tea.Int32(int32(describeVodUserDomainsPageSize)), } describeVodUserDomainsResp, err := d.sdkClient.DescribeVodUserDomainsWithContext(ctx, describeVodUserDomainsReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'vod.DescribeVodUserDomains'", slog.Any("request", describeVodUserDomainsReq), slog.Any("response", describeVodUserDomainsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'vod.DescribeLiveUserDomains': %w", err) } if describeVodUserDomainsResp.Body == nil || describeVodUserDomainsResp.Body.Domains == nil { break } for _, domainItem := range describeVodUserDomainsResp.Body.Domains.PageData { domains = append(domains, tea.StringValue(domainItem.DomainName)) } if len(describeVodUserDomainsResp.Body.Domains.PageData) < describeVodUserDomainsPageSize { break } describeVodUserDomainsPageNumber++ } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId int64, cloudCertName, cloudCertRegion string) error { // 设置域名证书 // REF: https://help.aliyun.com/zh/vod/developer-reference/api-vod-2017-03-21-setvoddomainsslcertificate setVodDomainSSLCertificateReq := &alivod.SetVodDomainSSLCertificateRequest{ DomainName: tea.String(domain), CertType: tea.String("cas"), CertId: tea.Int64(cloudCertId), CertName: tea.String(cloudCertName), CertRegion: tea.String(cloudCertRegion), SSLProtocol: tea.String("on"), } setVodDomainSSLCertificateResp, err := d.sdkClient.SetVodDomainSSLCertificateWithContext(ctx, setVodDomainSSLCertificateReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'live.SetVodDomainSSLCertificate'", slog.Any("request", setVodDomainSSLCertificateReq), slog.Any("response", setVodDomainSSLCertificateResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'live.SetVodDomainSSLCertificate': %w", err) } return nil } func createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.VodClient, error) { // 接入点一览 https://api.aliyun.com/product/vod var endpoint string switch region { case "": endpoint = "vod.cn-hangzhou.aliyuncs.com" default: endpoint = fmt.Sprintf("vod.%s.aliyuncs.com", region) } config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), Endpoint: tea.String(endpoint), } client, err := internal.NewVodClient(config) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/aliyun-vod/aliyun_vod_test.go ================================================ package aliyunvod_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-vod" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fRegion string fDomain string ) func init() { argsPrefix := "ALIYUNVOD_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./aliyun_vod_test.go -args \ --ALIYUNVOD_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --ALIYUNVOD_INPUTKEYPATH="/path/to/your-input-key.pem" \ --ALIYUNVOD_ACCESSKEYID="your-access-key-id" \ --ALIYUNVOD_ACCESSKEYSECRET="your-access-key-secret" \ --ALIYUNVOD_REGION="cn-hangzhou" \ --ALIYUNVOD_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Region: fRegion, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/aliyun-vod/consts.go ================================================ package aliyunvod const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/aliyun-vod/internal/client.go ================================================ package internal import ( "context" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" openapiutil "github.com/alibabacloud-go/darabonba-openapi/v2/utils" "github.com/alibabacloud-go/tea/dara" alivod "github.com/alibabacloud-go/vod-20170321/v4/client" ) // This is a partial copy of https://github.com/alibabacloud-go/vod-20170321/blob/master/client/client_context_func.go // to lightweight the vendor packages in the built binary. type VodClient struct { openapi.Client DisableSDKError *bool } func NewVodClient(config *openapiutil.Config) (*VodClient, error) { client := new(VodClient) err := client.Init(config) return client, err } func (client *VodClient) Init(config *openapiutil.Config) (_err error) { _err = client.Client.Init(config) if _err != nil { return _err } _err = client.CheckConfig(config) if _err != nil { return _err } return nil } func (client *VodClient) DescribeVodUserDomainsWithContext(ctx context.Context, request *alivod.DescribeVodUserDomainsRequest, runtime *dara.RuntimeOptions) (_result *alivod.DescribeVodUserDomainsResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.DomainName) { query["DomainName"] = request.DomainName } if !dara.IsNil(request.DomainSearchType) { query["DomainSearchType"] = request.DomainSearchType } if !dara.IsNil(request.DomainStatus) { query["DomainStatus"] = request.DomainStatus } if !dara.IsNil(request.OwnerId) { query["OwnerId"] = request.OwnerId } if !dara.IsNil(request.PageNumber) { query["PageNumber"] = request.PageNumber } if !dara.IsNil(request.PageSize) { query["PageSize"] = request.PageSize } if !dara.IsNil(request.SecurityToken) { query["SecurityToken"] = request.SecurityToken } if !dara.IsNil(request.Tag) { query["Tag"] = request.Tag } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("DescribeVodUserDomains"), Version: dara.String("2017-03-21"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alivod.DescribeVodUserDomainsResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *VodClient) SetVodDomainSSLCertificateWithContext(ctx context.Context, request *alivod.SetVodDomainSSLCertificateRequest, runtime *dara.RuntimeOptions) (_result *alivod.SetVodDomainSSLCertificateResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.CertId) { query["CertId"] = request.CertId } if !dara.IsNil(request.CertName) { query["CertName"] = request.CertName } if !dara.IsNil(request.CertRegion) { query["CertRegion"] = request.CertRegion } if !dara.IsNil(request.CertType) { query["CertType"] = request.CertType } if !dara.IsNil(request.DomainName) { query["DomainName"] = request.DomainName } if !dara.IsNil(request.Env) { query["Env"] = request.Env } if !dara.IsNil(request.OwnerId) { query["OwnerId"] = request.OwnerId } if !dara.IsNil(request.SSLPri) { query["SSLPri"] = request.SSLPri } if !dara.IsNil(request.SSLProtocol) { query["SSLProtocol"] = request.SSLProtocol } if !dara.IsNil(request.SSLPub) { query["SSLPub"] = request.SSLPub } if !dara.IsNil(request.SecurityToken) { query["SecurityToken"] = request.SecurityToken } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("SetVodDomainSSLCertificate"), Version: dara.String("2017-03-21"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &alivod.SetVodDomainSSLCertificateResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } ================================================ FILE: pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go ================================================ package aliyunwaf import ( "context" "errors" "fmt" "log/slog" "strings" "time" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" "github.com/alibabacloud-go/tea/dara" "github.com/alibabacloud-go/tea/tea" aliwaf "github.com/alibabacloud-go/waf-openapi-20211001/v7/client" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aliyun-cas" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-waf/internal" ) type DeployerConfig struct { // 阿里云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 阿里云资源组 ID。 ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 服务版本。 // 可取值 "3.0"。 ServiceVersion string `json:"serviceVersion"` // 服务类型。 ServiceType string `json:"serviceType"` // WAF 实例 ID。 InstanceId string `json:"instanceId"` // 云产品类型。 // 服务类型为 [SERVICE_TYPE_CLOUDRESOURCE] 时必填。 ResourceProduct string `json:"resourceProduct,omitempty"` // 云产品资源 ID。 // 服务类型为 [SERVICE_TYPE_CLOUDRESOURCE] 时必填。 ResourceId string `json:"resourceId,omitempty"` // 云产品资源端口。 // 服务类型为 [SERVICE_TYPE_CLOUDRESOURCE] 时必填。 ResourcePort int32 `json:"resourcePort,omitempty"` // 扩展域名(支持泛域名)。 Domain string `json:"domain,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.WafClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, ResourceGroupId: config.ResourceGroupId, Region: lo. If(config.Region == "" || strings.HasPrefix(config.Region, "cn-"), "cn-hangzhou"). Else("ap-southeast-1"), }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { switch d.config.ServiceVersion { case "3", "3.0": if err := d.deployToWAF3(ctx, certPEM, privkeyPEM); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported service version '%s'", d.config.ServiceVersion) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToWAF3(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.InstanceId == "" { return errors.New("config `instanceId` is required") } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 根据接入方式决定部署方式 switch d.config.ServiceType { case SERVICE_TYPE_CLOUDRESOURCE: certId := upres.ExtendedData["CertIdentifier"].(string) if err := d.deployToWAF3WithCloudResource(ctx, certId); err != nil { return err } case SERVICE_TYPE_CNAME: certId := upres.ExtendedData["CertIdentifier"].(string) if err := d.deployToWAF3WithCNAME(ctx, certId); err != nil { return err } default: return fmt.Errorf("unsupported service version '%s'", d.config.ServiceVersion) } return nil } func (d *Deployer) deployToWAF3WithCloudResource(ctx context.Context, cloudCertId string) error { if d.config.ResourceProduct == "" { return errors.New("config `resourceProduct` is required") } if d.config.ResourceId == "" { return errors.New("config `resourceId` is required") } if d.config.ResourcePort == 0 { return errors.New("config `resourcePort` is required") } // 查询云产品实例已同步的证书列表 // REF: https://www.alibabacloud.com/help/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-describeresourceinstancecerts // // 注意,虽然文档中描述为分页查询,但实际调用不支持分页 // https://github.com/certimate-go/certimate/issues/1122 var wafResourceInstanceCertificates []*aliwaf.DescribeResourceInstanceCertsResponseBodyCerts describeResourceInstanceCertsReq := &aliwaf.DescribeResourceInstanceCertsRequest{ RegionId: tea.String(d.config.Region), ResourceManagerResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId), InstanceId: tea.String(d.config.InstanceId), ResourceInstanceId: tea.String(d.config.ResourceId), } describeResourceInstanceCertsResp, err := d.sdkClient.DescribeResourceInstanceCertsWithContext(ctx, describeResourceInstanceCertsReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'waf.DescribeResourceInstanceCerts'", slog.Any("request", describeResourceInstanceCertsReq), slog.Any("response", describeResourceInstanceCertsResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'waf.DescribeResourceInstanceCerts': %w", err) } else { wafResourceInstanceCertificates = describeResourceInstanceCertsResp.Body.Certs } // 获取云产品实例的接入端口详情 // REF: https://www.alibabacloud.com/help/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-describecloudresourceaccessportdetails var wafCloudResourceCloudAccessPort *aliwaf.DescribeCloudResourceAccessPortDetailsResponseBodyAccessPortDetails var wafCloudResourceCertificates []*aliwaf.DescribeCloudResourceAccessPortDetailsResponseBodyAccessPortDetailsCertificates describeCloudResourceAccessPortDetailsRequest := &aliwaf.DescribeCloudResourceAccessPortDetailsRequest{ RegionId: tea.String(d.config.Region), ResourceManagerResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId), InstanceId: tea.String(d.config.InstanceId), ResourceInstanceId: tea.String(d.config.ResourceId), Port: tea.String(fmt.Sprintf("%d", d.config.ResourcePort)), } describeCloudResourceAccessPortDetailsResponse, err := d.sdkClient.DescribeCloudResourceAccessPortDetailsWithContext(ctx, describeCloudResourceAccessPortDetailsRequest, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'waf.DescribeCloudResourceAccessPortDetails'", slog.Any("request", describeCloudResourceAccessPortDetailsRequest), slog.Any("response", describeCloudResourceAccessPortDetailsResponse)) if err != nil { return fmt.Errorf("failed to execute sdk request 'waf.DescribeCloudResourceAccessPortDetails': %w", err) } else if len(describeCloudResourceAccessPortDetailsResponse.Body.AccessPortDetails) == 0 { return fmt.Errorf("could not get access port details of waf '%s' cloud resource '%s %s:%d'", d.config.InstanceId, d.config.ResourceProduct, d.config.ResourceId, d.config.ResourcePort) } else { wafCloudResourceCloudAccessPort = describeCloudResourceAccessPortDetailsResponse.Body.AccessPortDetails[0] wafCloudResourceCertificates = wafCloudResourceCloudAccessPort.Certificates if len(wafCloudResourceCertificates) == 0 { return fmt.Errorf("could not get access port certificates of waf '%s' cloud resource '%s %s:%d'", d.config.InstanceId, d.config.ResourceProduct, d.config.ResourceId, d.config.ResourcePort) } } // 生成请求参数 modifyCloudResourceCertReq := &aliwaf.ModifyCloudResourceCertRequest{ RegionId: tea.String(d.config.Region), InstanceId: tea.String(d.config.InstanceId), CloudResourceId: wafCloudResourceCloudAccessPort.CloudResourceId, } if d.config.Domain == "" { // 未指定扩展域名,只需替换默认证书 const certAppliedTypeDefault = "default" // 已部署过,直接跳过更新 for _, certItem := range wafCloudResourceCertificates { if tea.StringValue(certItem.AppliedType) == certAppliedTypeDefault && tea.StringValue(certItem.CertificateId) == cloudCertId { return nil } } // 移除原默认证书,添加新默认证书 modifyCloudResourceCertReq.Certificates = lo.Map(wafCloudResourceCertificates, func(c *aliwaf.DescribeCloudResourceAccessPortDetailsResponseBodyAccessPortDetailsCertificates, _ int) *aliwaf.ModifyCloudResourceCertRequestCertificates { return &aliwaf.ModifyCloudResourceCertRequestCertificates{ CertificateId: c.CertificateId, AppliedType: c.AppliedType, } }) modifyCloudResourceCertReq.Certificates = lo.Filter(modifyCloudResourceCertReq.Certificates, func(c *aliwaf.ModifyCloudResourceCertRequestCertificates, _ int) bool { if tea.StringValue(c.AppliedType) == certAppliedTypeDefault { return false } return true }) modifyCloudResourceCertReq.Certificates = append(modifyCloudResourceCertReq.Certificates, &aliwaf.ModifyCloudResourceCertRequestCertificates{ CertificateId: tea.String(cloudCertId), AppliedType: tea.String(certAppliedTypeDefault), }) } else { // 指定扩展域名,替换或新增扩展证书 const certAppliedTypeExtension = "extension" // 已部署过,直接跳过更新 for _, certItem := range wafCloudResourceCertificates { if tea.StringValue(certItem.AppliedType) == certAppliedTypeExtension && tea.StringValue(certItem.CertificateId) == cloudCertId { return nil } } // 移除同 CommonName 的原扩展证书,添加新扩展证书 modifyCloudResourceCertReq.Certificates = lo.Map(wafCloudResourceCertificates, func(c *aliwaf.DescribeCloudResourceAccessPortDetailsResponseBodyAccessPortDetailsCertificates, _ int) *aliwaf.ModifyCloudResourceCertRequestCertificates { return &aliwaf.ModifyCloudResourceCertRequestCertificates{ CertificateId: c.CertificateId, AppliedType: c.AppliedType, } }) modifyCloudResourceCertReq.Certificates = lo.Filter(modifyCloudResourceCertReq.Certificates, func(c *aliwaf.ModifyCloudResourceCertRequestCertificates, _ int) bool { if tea.StringValue(c.AppliedType) == certAppliedTypeExtension { if tea.StringValue(c.CertificateId) == cloudCertId { return false } var certCommonName string for _, r := range wafResourceInstanceCertificates { if tea.StringValue(c.CertificateId) == tea.StringValue(r.CertIdentifier) { certCommonName = tea.StringValue(r.CommonName) break } } if certCommonName == d.config.Domain { return false } } return true }) modifyCloudResourceCertReq.Certificates = append(modifyCloudResourceCertReq.Certificates, &aliwaf.ModifyCloudResourceCertRequestCertificates{ CertificateId: tea.String(cloudCertId), AppliedType: tea.String(certAppliedTypeExtension), }) } // 过滤掉不存在或已过期的证书,防止接口报错 modifyCloudResourceCertReq.Certificates = lo.Filter(modifyCloudResourceCertReq.Certificates, func(c *aliwaf.ModifyCloudResourceCertRequestCertificates, _ int) bool { if tea.StringValue(c.CertificateId) == cloudCertId { return true } resourceInstanceCert, _ := lo.Find(wafResourceInstanceCertificates, func(r *aliwaf.DescribeResourceInstanceCertsResponseBodyCerts) bool { cId := tea.StringValue(c.CertificateId) rId := tea.StringValue(r.CertIdentifier) return cId == rId || strings.Split(cId, "-")[0] == strings.Split(rId, "-")[0] }) if resourceInstanceCert != nil { certNotAfter := time.Unix(tea.Int64Value(resourceInstanceCert.AfterDate)/1000, 0) return certNotAfter.After(time.Now()) } return false }) // 修改云产品接入的证书 // REF: https://www.alibabacloud.com/help/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-modifycloudresourcecert modifyCloudResourceCertResp, err := d.sdkClient.ModifyCloudResourceCertWithContext(ctx, modifyCloudResourceCertReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'waf.ModifyCloudResourceCert'", slog.Any("request", modifyCloudResourceCertReq), slog.Any("response", modifyCloudResourceCertResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'waf.ModifyCloudResourceCert': %w", err) } return nil } func (d *Deployer) deployToWAF3WithCNAME(ctx context.Context, cloudCertId string) error { if d.config.Domain == "" { // 未指定扩展域名,只需替换默认证书 // 查询默认 SSL/TLS 设置 // REF: https://help.aliyun.com/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-describedefaulthttps describeDefaultHttpsReq := &aliwaf.DescribeDefaultHttpsRequest{ ResourceManagerResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId), RegionId: tea.String(d.config.Region), InstanceId: tea.String(d.config.InstanceId), } describeDefaultHttpsResp, err := d.sdkClient.DescribeDefaultHttpsWithContext(ctx, describeDefaultHttpsReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'waf.DescribeDefaultHttps'", slog.Any("request", describeDefaultHttpsReq), slog.Any("response", describeDefaultHttpsResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'waf.DescribeDefaultHttps': %w", err) } // 修改默认 SSL/TLS 设置 // REF: https://help.aliyun.com/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-modifydefaulthttps modifyDefaultHttpsReq := &aliwaf.ModifyDefaultHttpsRequest{ ResourceManagerResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId), RegionId: tea.String(d.config.Region), InstanceId: tea.String(d.config.InstanceId), CertId: tea.String(cloudCertId), TLSVersion: tea.String("tlsv1.2"), EnableTLSv3: tea.Bool(true), } if describeDefaultHttpsResp.Body != nil && describeDefaultHttpsResp.Body.DefaultHttps != nil { if describeDefaultHttpsResp.Body.DefaultHttps.TLSVersion != nil { modifyDefaultHttpsReq.TLSVersion = describeDefaultHttpsResp.Body.DefaultHttps.TLSVersion } if describeDefaultHttpsResp.Body.DefaultHttps.EnableTLSv3 == nil { modifyDefaultHttpsReq.EnableTLSv3 = describeDefaultHttpsResp.Body.DefaultHttps.EnableTLSv3 } } modifyDefaultHttpsResp, err := d.sdkClient.ModifyDefaultHttpsWithContext(ctx, modifyDefaultHttpsReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'waf.ModifyDefaultHttps'", slog.Any("request", modifyDefaultHttpsReq), slog.Any("response", modifyDefaultHttpsResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'waf.ModifyDefaultHttps': %w", err) } } else { // 指定扩展域名,需替换扩展证书 // 查询 CNAME 接入详情 // REF: https://help.aliyun.com/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-describedomaindetail describeDomainDetailReq := &aliwaf.DescribeDomainDetailRequest{ RegionId: tea.String(d.config.Region), InstanceId: tea.String(d.config.InstanceId), Domain: tea.String(d.config.Domain), } describeDomainDetailResp, err := d.sdkClient.DescribeDomainDetailWithContext(ctx, describeDomainDetailReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'waf.DescribeDomainDetail'", slog.Any("request", describeDomainDetailReq), slog.Any("response", describeDomainDetailResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'waf.DescribeDomainDetail': %w", err) } // 修改 CNAME 接入资源 // REF: https://help.aliyun.com/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-modifydomain modifyDomainReq := &aliwaf.ModifyDomainRequest{ RegionId: tea.String(d.config.Region), InstanceId: tea.String(d.config.InstanceId), Domain: tea.String(d.config.Domain), Listen: &aliwaf.ModifyDomainRequestListen{CertId: tea.String(cloudCertId)}, Redirect: &aliwaf.ModifyDomainRequestRedirect{Loadbalance: tea.String("iphash")}, } modifyDomainReq = _assign(modifyDomainReq, describeDomainDetailResp.Body) modifyDomainResp, err := d.sdkClient.ModifyDomainWithContext(ctx, modifyDomainReq, &dara.RuntimeOptions{}) d.logger.Debug("sdk request 'waf.ModifyDomain'", slog.Any("request", modifyDomainReq), slog.Any("response", modifyDomainResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'waf.ModifyDomain': %w", err) } } return nil } func createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.WafClient, error) { // 接入点一览:https://api.aliyun.com/product/waf-openapi var endpoint string switch region { case "": endpoint = "wafopenapi.cn-hangzhou.aliyuncs.com" default: endpoint = fmt.Sprintf("wafopenapi.%s.aliyuncs.com", region) } config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), Endpoint: tea.String(endpoint), } client, err := internal.NewWafClient(config) if err != nil { return nil, err } return client, nil } func _assign(source *aliwaf.ModifyDomainRequest, target *aliwaf.DescribeDomainDetailResponseBody) *aliwaf.ModifyDomainRequest { // `ModifyDomain` 中不传的字段表示使用默认值、而非保留原值, // 因此这里需要把原配置中的参数重新赋值回去。 if target == nil { return source } if target.Listen != nil { if source.Listen == nil { source.Listen = &aliwaf.ModifyDomainRequestListen{} } if target.Listen.CipherSuite != nil { source.Listen.CipherSuite = tea.Int32(int32(*target.Listen.CipherSuite)) } if target.Listen.CustomCiphers != nil { source.Listen.CustomCiphers = target.Listen.CustomCiphers } if target.Listen.EnableTLSv3 != nil { source.Listen.EnableTLSv3 = target.Listen.EnableTLSv3 } if target.Listen.ExclusiveIp != nil { source.Listen.ExclusiveIp = target.Listen.ExclusiveIp } if target.Listen.FocusHttps != nil { source.Listen.FocusHttps = target.Listen.FocusHttps } if target.Listen.Http2Enabled != nil { source.Listen.Http2Enabled = target.Listen.Http2Enabled } if target.Listen.HttpPorts != nil { source.Listen.HttpPorts = lo.Map(target.Listen.HttpPorts, func(v *int64, _ int) *int32 { if v == nil { return nil } return tea.Int32(int32(*v)) }) } if target.Listen.HttpsPorts != nil { source.Listen.HttpsPorts = lo.Map(target.Listen.HttpsPorts, func(v *int64, _ int) *int32 { if v == nil { return nil } return tea.Int32(int32(*v)) }) } if target.Listen.IPv6Enabled != nil { source.Listen.IPv6Enabled = target.Listen.IPv6Enabled } if target.Listen.ProtectionResource != nil { source.Listen.ProtectionResource = target.Listen.ProtectionResource } if target.Listen.TLSVersion != nil { source.Listen.TLSVersion = target.Listen.TLSVersion } if target.Listen.XffHeaderMode != nil { source.Listen.XffHeaderMode = tea.Int32(int32(*target.Listen.XffHeaderMode)) } if target.Listen.XffHeaders != nil { source.Listen.XffHeaders = target.Listen.XffHeaders } } if target.Redirect != nil { if source.Redirect == nil { source.Redirect = &aliwaf.ModifyDomainRequestRedirect{} } if target.Redirect.Backends != nil { source.Redirect.Backends = lo.Map(target.Redirect.Backends, func(v *aliwaf.DescribeDomainDetailResponseBodyRedirectBackends, _ int) *string { if v == nil { return nil } return v.Backend }) } if target.Redirect.BackupBackends != nil { source.Redirect.BackupBackends = lo.Map(target.Redirect.BackupBackends, func(v *aliwaf.DescribeDomainDetailResponseBodyRedirectBackupBackends, _ int) *string { if v == nil { return nil } return v.Backend }) } if target.Redirect.ConnectTimeout != nil { source.Redirect.ConnectTimeout = target.Redirect.ConnectTimeout } if target.Redirect.FocusHttpBackend != nil { source.Redirect.FocusHttpBackend = target.Redirect.FocusHttpBackend } if target.Redirect.Keepalive != nil { source.Redirect.Keepalive = target.Redirect.Keepalive } if target.Redirect.KeepaliveRequests != nil { source.Redirect.KeepaliveRequests = target.Redirect.KeepaliveRequests } if target.Redirect.KeepaliveTimeout != nil { source.Redirect.KeepaliveTimeout = target.Redirect.KeepaliveTimeout } if target.Redirect.Loadbalance != nil { source.Redirect.Loadbalance = target.Redirect.Loadbalance } if target.Redirect.ReadTimeout != nil { source.Redirect.ReadTimeout = target.Redirect.ReadTimeout } if target.Redirect.RequestHeaders != nil { source.Redirect.RequestHeaders = lo.Map(target.Redirect.RequestHeaders, func(v *aliwaf.DescribeDomainDetailResponseBodyRedirectRequestHeaders, _ int) *aliwaf.ModifyDomainRequestRedirectRequestHeaders { if v == nil { return nil } return &aliwaf.ModifyDomainRequestRedirectRequestHeaders{ Key: v.Key, Value: v.Value, } }) } if target.Redirect.Retry != nil { source.Redirect.Retry = target.Redirect.Retry } if target.Redirect.SniEnabled != nil { source.Redirect.SniEnabled = target.Redirect.SniEnabled } if target.Redirect.SniHost != nil { source.Redirect.SniHost = target.Redirect.SniHost } if target.Redirect.WriteTimeout != nil { source.Redirect.WriteTimeout = target.Redirect.WriteTimeout } if target.Redirect.XffProto != nil { source.Redirect.XffProto = target.Redirect.XffProto } } return source } ================================================ FILE: pkg/core/deployer/providers/aliyun-waf/aliyun_waf_test.go ================================================ package aliyunwaf_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/aliyun-waf" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fRegion string fInstanceId string ) func init() { argsPrefix := "ALIYUNWAF_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fInstanceId, argsPrefix+"INSTANCEID", "", "") } /* Shell command to run this test: go test -v ./aliyun_waf_test.go -args \ --ALIYUNWAF_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --ALIYUNWAF_INPUTKEYPATH="/path/to/your-input-key.pem" \ --ALIYUNWAF_ACCESSKEYID="your-access-key-id" \ --ALIYUNWAF_ACCESSKEYSECRET="your-access-key-secret" \ --ALIYUNWAF_REGION="cn-hangzhou" \ --ALIYUNWAF_INSTANCEID="your-waf-instance-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("INSTANCEID: %v", fInstanceId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Region: fRegion, InstanceId: fInstanceId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/aliyun-waf/consts.go ================================================ package aliyunwaf const ( // 服务类型:云产品接入。 SERVICE_TYPE_CLOUDRESOURCE = "cloudresource" // 服务类型:CNAME 接入。 SERVICE_TYPE_CNAME = "cname" ) ================================================ FILE: pkg/core/deployer/providers/aliyun-waf/internal/client.go ================================================ package internal import ( "context" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" openapiutil "github.com/alibabacloud-go/darabonba-openapi/v2/utils" "github.com/alibabacloud-go/tea/dara" aliwaf "github.com/alibabacloud-go/waf-openapi-20211001/v7/client" ) // This is a partial copy of https://github.com/alibabacloud-go/waf-openapi-20211001/blob/master/client/client_context_func.go // to lightweight the vendor packages in the built binary. type WafClient struct { openapi.Client DisableSDKError *bool } func NewWafClient(config *openapiutil.Config) (*WafClient, error) { client := new(WafClient) err := client.Init(config) return client, err } func (client *WafClient) Init(config *openapiutil.Config) (_err error) { _err = client.Client.Init(config) if _err != nil { return _err } _err = client.CheckConfig(config) if _err != nil { return _err } return nil } func (client *WafClient) DescribeCloudResourceAccessPortDetailsWithContext(ctx context.Context, request *aliwaf.DescribeCloudResourceAccessPortDetailsRequest, runtime *dara.RuntimeOptions) (_result *aliwaf.DescribeCloudResourceAccessPortDetailsResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.InstanceId) { query["InstanceId"] = request.InstanceId } if !dara.IsNil(request.PageNumber) { query["PageNumber"] = request.PageNumber } if !dara.IsNil(request.PageSize) { query["PageSize"] = request.PageSize } if !dara.IsNil(request.Port) { query["Port"] = request.Port } if !dara.IsNil(request.Protocol) { query["Protocol"] = request.Protocol } if !dara.IsNil(request.RegionId) { query["RegionId"] = request.RegionId } if !dara.IsNil(request.ResourceInstanceId) { query["ResourceInstanceId"] = request.ResourceInstanceId } if !dara.IsNil(request.ResourceManagerResourceGroupId) { query["ResourceManagerResourceGroupId"] = request.ResourceManagerResourceGroupId } if !dara.IsNil(request.ResourceProduct) { query["ResourceProduct"] = request.ResourceProduct } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("DescribeCloudResourceAccessPortDetails"), Version: dara.String("2021-10-01"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &aliwaf.DescribeCloudResourceAccessPortDetailsResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *WafClient) DescribeDefaultHttpsWithContext(ctx context.Context, request *aliwaf.DescribeDefaultHttpsRequest, runtime *dara.RuntimeOptions) (_result *aliwaf.DescribeDefaultHttpsResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.InstanceId) { query["InstanceId"] = request.InstanceId } if !dara.IsNil(request.RegionId) { query["RegionId"] = request.RegionId } if !dara.IsNil(request.ResourceManagerResourceGroupId) { query["ResourceManagerResourceGroupId"] = request.ResourceManagerResourceGroupId } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("DescribeDefaultHttps"), Version: dara.String("2021-10-01"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &aliwaf.DescribeDefaultHttpsResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *WafClient) DescribeDomainDetailWithContext(ctx context.Context, request *aliwaf.DescribeDomainDetailRequest, runtime *dara.RuntimeOptions) (_result *aliwaf.DescribeDomainDetailResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.Domain) { query["Domain"] = request.Domain } if !dara.IsNil(request.DomainId) { query["DomainId"] = request.DomainId } if !dara.IsNil(request.InstanceId) { query["InstanceId"] = request.InstanceId } if !dara.IsNil(request.RegionId) { query["RegionId"] = request.RegionId } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("DescribeDomainDetail"), Version: dara.String("2021-10-01"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &aliwaf.DescribeDomainDetailResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *WafClient) DescribeResourceInstanceCertsWithContext(ctx context.Context, request *aliwaf.DescribeResourceInstanceCertsRequest, runtime *dara.RuntimeOptions) (_result *aliwaf.DescribeResourceInstanceCertsResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.InstanceId) { query["InstanceId"] = request.InstanceId } if !dara.IsNil(request.PageNumber) { query["PageNumber"] = request.PageNumber } if !dara.IsNil(request.PageSize) { query["PageSize"] = request.PageSize } if !dara.IsNil(request.RegionId) { query["RegionId"] = request.RegionId } if !dara.IsNil(request.ResourceInstanceId) { query["ResourceInstanceId"] = request.ResourceInstanceId } if !dara.IsNil(request.ResourceManagerResourceGroupId) { query["ResourceManagerResourceGroupId"] = request.ResourceManagerResourceGroupId } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("DescribeResourceInstanceCerts"), Version: dara.String("2021-10-01"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &aliwaf.DescribeResourceInstanceCertsResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *WafClient) ModifyCloudResourceCertWithContext(ctx context.Context, request *aliwaf.ModifyCloudResourceCertRequest, runtime *dara.RuntimeOptions) (_result *aliwaf.ModifyCloudResourceCertResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.Certificates) { query["Certificates"] = request.Certificates } if !dara.IsNil(request.CloudResourceId) { query["CloudResourceId"] = request.CloudResourceId } if !dara.IsNil(request.InstanceId) { query["InstanceId"] = request.InstanceId } if !dara.IsNil(request.Port) { query["Port"] = request.Port } if !dara.IsNil(request.RegionId) { query["RegionId"] = request.RegionId } if !dara.IsNil(request.ResourceInstanceId) { query["ResourceInstanceId"] = request.ResourceInstanceId } if !dara.IsNil(request.ResourceProduct) { query["ResourceProduct"] = request.ResourceProduct } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("ModifyCloudResourceCert"), Version: dara.String("2021-10-01"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &aliwaf.ModifyCloudResourceCertResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *WafClient) ModifyDefaultHttpsWithContext(ctx context.Context, request *aliwaf.ModifyDefaultHttpsRequest, runtime *dara.RuntimeOptions) (_result *aliwaf.ModifyDefaultHttpsResponse, _err error) { _err = request.Validate() if _err != nil { return _result, _err } query := map[string]interface{}{} if !dara.IsNil(request.CertId) { query["CertId"] = request.CertId } if !dara.IsNil(request.CipherSuite) { query["CipherSuite"] = request.CipherSuite } if !dara.IsNil(request.CustomCiphers) { query["CustomCiphers"] = request.CustomCiphers } if !dara.IsNil(request.EnableTLSv3) { query["EnableTLSv3"] = request.EnableTLSv3 } if !dara.IsNil(request.InstanceId) { query["InstanceId"] = request.InstanceId } if !dara.IsNil(request.RegionId) { query["RegionId"] = request.RegionId } if !dara.IsNil(request.ResourceManagerResourceGroupId) { query["ResourceManagerResourceGroupId"] = request.ResourceManagerResourceGroupId } if !dara.IsNil(request.TLSVersion) { query["TLSVersion"] = request.TLSVersion } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("ModifyDefaultHttps"), Version: dara.String("2021-10-01"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &aliwaf.ModifyDefaultHttpsResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } func (client *WafClient) ModifyDomainWithContext(ctx context.Context, tmpReq *aliwaf.ModifyDomainRequest, runtime *dara.RuntimeOptions) (_result *aliwaf.ModifyDomainResponse, _err error) { _err = tmpReq.Validate() if _err != nil { return _result, _err } request := &aliwaf.ModifyDomainShrinkRequest{} openapiutil.Convert(tmpReq, request) if !dara.IsNil(tmpReq.Listen) { request.ListenShrink = openapiutil.ArrayToStringWithSpecifiedStyle(tmpReq.Listen, dara.String("Listen"), dara.String("json")) } if !dara.IsNil(tmpReq.Redirect) { request.RedirectShrink = openapiutil.ArrayToStringWithSpecifiedStyle(tmpReq.Redirect, dara.String("Redirect"), dara.String("json")) } query := map[string]interface{}{} if !dara.IsNil(request.AccessType) { query["AccessType"] = request.AccessType } if !dara.IsNil(request.Domain) { query["Domain"] = request.Domain } if !dara.IsNil(request.DomainId) { query["DomainId"] = request.DomainId } if !dara.IsNil(request.InstanceId) { query["InstanceId"] = request.InstanceId } if !dara.IsNil(request.ListenShrink) { query["Listen"] = request.ListenShrink } if !dara.IsNil(request.RedirectShrink) { query["Redirect"] = request.RedirectShrink } if !dara.IsNil(request.RegionId) { query["RegionId"] = request.RegionId } req := &openapiutil.OpenApiRequest{ Query: openapiutil.Query(query), } params := &openapiutil.Params{ Action: dara.String("ModifyDomain"), Version: dara.String("2021-10-01"), Protocol: dara.String("HTTPS"), Pathname: dara.String("/"), Method: dara.String("POST"), AuthType: dara.String("AK"), Style: dara.String("RPC"), ReqBodyType: dara.String("formData"), BodyType: dara.String("json"), } _result = &aliwaf.ModifyDomainResponse{} _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) if _err != nil { return _result, _err } _err = dara.Convert(_body, &_result) return _result, _err } ================================================ FILE: pkg/core/deployer/providers/apisix/apisix.go ================================================ package apisix import ( "context" "crypto/tls" "errors" "fmt" "log/slog" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/deployer" apisixsdk "github.com/certimate-go/certimate/pkg/sdk3rd/apisix" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type DeployerConfig struct { // APISIX 服务地址。 ServerUrl string `json:"serverUrl"` // APISIX Admin API Key。 ApiKey string `json:"apiKey"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 证书 ID。 // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。 CertificateId string `json:"certificateId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *apisixsdk.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections) 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) (*deployer.DeployResult, error) { // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_CERTIFICATE: if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.CertificateId == "" { return errors.New("config `certificateId` is required") } // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return err } // 更新 SSL 证书 // REF: https://apisix.apache.org/zh/docs/apisix/admin-api/#ssl sslUpdateReq := &apisixsdk.SslUpdateRequest{ ID: lo.ToPtr(d.config.CertificateId), Certificate: lo.ToPtr(certPEM), PrivateKey: lo.ToPtr(privkeyPEM), SNIs: lo.ToPtr(certX509.DNSNames), Type: lo.ToPtr("server"), Status: lo.ToPtr(int32(1)), } sslUpdateResp, err := d.sdkClient.SslUpdateWithContext(ctx, d.config.CertificateId, sslUpdateReq) d.logger.Debug("sdk request 'apisix.SslUpdate'", slog.Any("request", sslUpdateReq), slog.Any("response", sslUpdateResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'apisix.SslUpdate': %w", err) } return nil } func createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*apisixsdk.Client, error) { client, err := apisixsdk.NewClient(serverUrl, apiKey) if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } ================================================ FILE: pkg/core/deployer/providers/apisix/apisix_test.go ================================================ package apisix_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/apisix" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fApiKey string fCertificateId string ) func init() { argsPrefix := "APISIX_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "") } /* Shell command to run this test: go test -v ./apisix_test.go -args \ --APISIX_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --APISIX_INPUTKEYPATH="/path/to/your-input-key.pem" \ --APISIX_SERVERURL="http://127.0.0.1:9080" \ --APISIX_APIKEY="your-api-key" \ --APISIX_CERTIFICATEID="your-certificate-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("APIKEY: %v", fApiKey), fmt.Sprintf("CERTIFICATEID: %v", fCertificateId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ServerUrl: fServerUrl, ApiKey: fApiKey, AllowInsecureConnections: true, ResourceType: provider.RESOURCE_TYPE_CERTIFICATE, CertificateId: fCertificateId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/apisix/consts.go ================================================ package apisix const ( // 资源类型:替换指定证书。 RESOURCE_TYPE_CERTIFICATE = "certificate" ) ================================================ FILE: pkg/core/deployer/providers/aws-acm/aws_acm.go ================================================ package awsacm import ( "context" "errors" "fmt" "log/slog" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aws-acm" "github.com/certimate-go/certimate/pkg/core/deployer" ) type DeployerConfig struct { // AWS AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // AWS SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // AWS 区域。 Region string `json:"region"` // ACM 证书 ARN。 // 选填。零值时表示新建证书;否则表示更新证书。 CertificateArn string `json:"certificateArn,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, SecretAccessKey: config.SecretAccessKey, Region: config.Region, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { if d.config.CertificateArn == "" { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } } else { // 替换证书 opres, err := d.sdkCertmgr.Replace(ctx, d.config.CertificateArn, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to replace certificate file: %w", err) } else { d.logger.Info("ssl certificate replaced", slog.Any("result", opres)) } } return &deployer.DeployResult{}, nil } ================================================ FILE: pkg/core/deployer/providers/aws-cloudfront/aws_cloudfront.go ================================================ package awscloudfront import ( "context" "errors" "fmt" "log/slog" aws "github.com/aws/aws-sdk-go-v2/aws" awscfg "github.com/aws/aws-sdk-go-v2/config" awscred "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgracm "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aws-acm" mcertmgriam "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aws-iam" "github.com/certimate-go/certimate/pkg/core/deployer" ) type DeployerConfig struct { // AWS AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // AWS SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // AWS 区域。 Region string `json:"region"` // AWS CloudFront 分配 ID。 DistributionId string `json:"distributionId"` // AWS CloudFront 证书来源。 // 可取值 "ACM"、"IAM"。 CertificateSource string `json:"certificateSource"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *cloudfront.Client sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } var pcertmgr certmgr.Provider switch config.CertificateSource { case CERTIFICATE_SOURCE_ACM: pcertmgr, err = mcertmgracm.NewCertmgr(&mcertmgracm.CertmgrConfig{ AccessKeyId: config.AccessKeyId, SecretAccessKey: config.SecretAccessKey, Region: config.Region, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } case CERTIFICATE_SOURCE_IAM: pcertmgr, err = mcertmgriam.NewCertmgr(&mcertmgriam.CertmgrConfig{ AccessKeyId: config.AccessKeyId, SecretAccessKey: config.SecretAccessKey, Region: config.Region, CertificatePath: "/cloudfront/", }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } default: return nil, fmt.Errorf("unsupported certificate source: '%s'", config.CertificateSource) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { if d.config.DistributionId == "" { return nil, errors.New("config `distribuitionId` is required") } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取分配配置 // REF: https://docs.aws.amazon.com/en_us/cloudfront/latest/APIReference/API_GetDistributionConfig.html getDistributionConfigReq := &cloudfront.GetDistributionConfigInput{ Id: aws.String(d.config.DistributionId), } getDistributionConfigResp, err := d.sdkClient.GetDistributionConfig(ctx, getDistributionConfigReq) d.logger.Debug("sdk request 'cloudfront.GetDistributionConfig'", slog.Any("request", getDistributionConfigReq), slog.Any("response", getDistributionConfigResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cloudfront.GetDistributionConfig': %w", err) } // 更新分配配置 // REF: https://docs.aws.amazon.com/zh_cn/cloudfront/latest/APIReference/API_UpdateDistribution.html updateDistributionReq := &cloudfront.UpdateDistributionInput{ Id: aws.String(d.config.DistributionId), DistributionConfig: getDistributionConfigResp.DistributionConfig, IfMatch: getDistributionConfigResp.ETag, } if updateDistributionReq.DistributionConfig.ViewerCertificate == nil { updateDistributionReq.DistributionConfig.ViewerCertificate = &types.ViewerCertificate{} } updateDistributionReq.DistributionConfig.ViewerCertificate.CloudFrontDefaultCertificate = aws.Bool(false) switch d.config.CertificateSource { case CERTIFICATE_SOURCE_ACM: updateDistributionReq.DistributionConfig.ViewerCertificate.ACMCertificateArn = aws.String(upres.CertId) updateDistributionReq.DistributionConfig.ViewerCertificate.IAMCertificateId = nil case CERTIFICATE_SOURCE_IAM: updateDistributionReq.DistributionConfig.ViewerCertificate.ACMCertificateArn = nil updateDistributionReq.DistributionConfig.ViewerCertificate.IAMCertificateId = aws.String(upres.CertId) if updateDistributionReq.DistributionConfig.ViewerCertificate.MinimumProtocolVersion == "" { updateDistributionReq.DistributionConfig.ViewerCertificate.MinimumProtocolVersion = types.MinimumProtocolVersionTLSv122018 } if updateDistributionReq.DistributionConfig.ViewerCertificate.SSLSupportMethod == "" { updateDistributionReq.DistributionConfig.ViewerCertificate.SSLSupportMethod = types.SSLSupportMethodSniOnly } } updateDistributionResp, err := d.sdkClient.UpdateDistribution(ctx, updateDistributionReq) d.logger.Debug("sdk request 'cloudfront.UpdateDistribution'", slog.Any("request", updateDistributionReq), slog.Any("response", updateDistributionResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cloudfront.UpdateDistribution': %w", err) } return &deployer.DeployResult{}, nil } func createSDKClient(accessKeyId, secretAccessKey, region string) (*cloudfront.Client, error) { cfg, err := awscfg.LoadDefaultConfig(context.Background()) if err != nil { return nil, err } client := cloudfront.NewFromConfig(cfg, func(o *cloudfront.Options) { o.Region = region o.Credentials = aws.NewCredentialsCache(awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, "")) }) return client, nil } ================================================ FILE: pkg/core/deployer/providers/aws-cloudfront/aws_cloudfront_test.go ================================================ package awscloudfront_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/aws-cloudfront" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string fRegion string fDistribuitionId string ) func init() { argsPrefix := "AWSCLOUDFRONT_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fDistribuitionId, argsPrefix+"DISTRIBUTIONID", "", "") } /* Shell command to run this test: go test -v ./aws_cloudfront_test.go -args \ --AWSCLOUDFRONT_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --AWSCLOUDFRONT_INPUTKEYPATH="/path/to/your-input-key.pem" \ --AWSCLOUDFRONT_ACCESSKEYID="your-access-key-id" \ --AWSCLOUDFRONT_SECRETACCESSKEY="your-secret-access-id" \ --AWSCLOUDFRONT_REGION="us-east-1" \ --AWSCLOUDFRONT_DISTRIBUTIONID="your-distribution-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("DISTRIBUTIONID: %v", fDistribuitionId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, Region: fRegion, DistributionId: fDistribuitionId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/aws-cloudfront/consts.go ================================================ package awscloudfront const ( CERTIFICATE_SOURCE_ACM = "ACM" CERTIFICATE_SOURCE_IAM = "IAM" ) ================================================ FILE: pkg/core/deployer/providers/aws-iam/aws_iam.go ================================================ package awsiam import ( "context" "errors" "fmt" "log/slog" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aws-iam" "github.com/certimate-go/certimate/pkg/core/deployer" ) type DeployerConfig struct { // AWS AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // AWS SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // AWS 区域。 Region string `json:"region"` // IAM 证书路径。 // 选填。 CertificatePath string `json:"certificatePath,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, SecretAccessKey: config.SecretAccessKey, Region: config.Region, CertificatePath: config.CertificatePath, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } return &deployer.DeployResult{}, nil } ================================================ FILE: pkg/core/deployer/providers/azure-keyvault/azure_keyvault.go ================================================ package azurekeyvault import ( "context" "errors" "fmt" "log/slog" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/azure-keyvault" "github.com/certimate-go/certimate/pkg/core/deployer" ) type DeployerConfig struct { // Azure TenantId。 TenantId string `json:"tenantId"` // Azure ClientId。 ClientId string `json:"clientId"` // Azure ClientSecret。 ClientSecret string `json:"clientSecret"` // Azure 主权云环境。 CloudName string `json:"cloudName,omitempty"` // Key Vault 名称。 KeyVaultName string `json:"keyvaultName"` // Key Vault 证书名称。 // 选填。零值时表示新建证书;否则表示更新证书。 CertificateName string `json:"certificateName,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ TenantId: config.TenantId, ClientId: config.ClientId, ClientSecret: config.ClientSecret, CloudName: config.CloudName, KeyVaultName: config.KeyVaultName, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { if d.config.CertificateName == "" { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } } else { // 替换证书 opres, err := d.sdkCertmgr.Replace(ctx, d.config.CertificateName, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to replace certificate file: %w", err) } else { d.logger.Info("ssl certificate replaced", slog.Any("result", opres)) } } return &deployer.DeployResult{}, nil } ================================================ FILE: pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go ================================================ package baiducloudappblb import ( "context" "errors" "fmt" "log/slog" "strconv" "strings" bceappblb "github.com/baidubce/bce-sdk-go/services/appblb" "github.com/pocketbase/pocketbase/tools/security" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/baiducloud-cert" "github.com/certimate-go/certimate/pkg/core/deployer" ) type DeployerConfig struct { // 百度智能云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 百度智能云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // 百度智能云区域。 Region string `json:"region"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 负载均衡实例 ID。 // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时必填。 LoadbalancerId string `json:"loadbalancerId,omitempty"` // 负载均衡监听端口。 // 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。 ListenerPort int32 `json:"listenerPort,omitempty"` // SNI 域名(支持泛域名)。 // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时选填。 Domain string `json:"domain,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *bceappblb.Client sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, SecretAccessKey: config.SecretAccessKey, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, 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) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_LOADBALANCER: if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil { return nil, err } case RESOURCE_TYPE_LISTENER: if err := d.deployToListener(ctx, upres.CertId); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error { if d.config.LoadbalancerId == "" { return errors.New("config `loadbalancerId` is required") } // 查询 BLB 实例详情 // REF: https://cloud.baidu.com/doc/BLB/s/6jwvxnyhi#describeloadbalancerdetail%E6%9F%A5%E8%AF%A2blb%E5%AE%9E%E4%BE%8B%E8%AF%A6%E6%83%85 describeLoadBalancerDetailResp, err := d.sdkClient.DescribeLoadBalancerDetail(d.config.LoadbalancerId) d.logger.Debug("sdk request 'appblb.DescribeLoadBalancerAttribute'", slog.String("blbId", d.config.LoadbalancerId), slog.Any("response", describeLoadBalancerDetailResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'appblb.DescribeLoadBalancerDetail': %w", err) } // 获取全部 HTTPS/SSL 监听端口 listeners := make([]struct { Type string Port int32 }, 0) for _, listener := range describeLoadBalancerDetailResp.Listener { if listener.Type == "HTTPS" || listener.Type == "SSL" { listenerPort, err := strconv.Atoi(listener.Port) if err != nil { continue } listeners = append(listeners, struct { Type string Port int32 }{ Type: listener.Type, Port: int32(listenerPort), }) } } // 遍历更新监听证书 if len(listeners) == 0 { d.logger.Info("no blb listeners to deploy") } else { d.logger.Info("found https/ssl listeners to deploy", slog.Any("listeners", listeners)) var errs []error for _, listener := range listeners { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error { if d.config.LoadbalancerId == "" { return errors.New("config `loadbalancerId` is required") } if d.config.ListenerPort == 0 { return errors.New("config `listenerPort` is required") } // 查询监听 // REF: https://cloud.baidu.com/doc/BLB/s/ujwvxnyux#describeappalllisteners%E6%9F%A5%E8%AF%A2%E6%89%80%E6%9C%89%E7%9B%91%E5%90%AC describeAppAllListenersRequest := &bceappblb.DescribeAppListenerArgs{ ListenerPort: uint16(d.config.ListenerPort), } describeAppAllListenersResp, err := d.sdkClient.DescribeAppAllListeners(d.config.LoadbalancerId, describeAppAllListenersRequest) d.logger.Debug("sdk request 'appblb.DescribeAppAllListeners'", slog.String("blbId", d.config.LoadbalancerId), slog.Any("request", describeAppAllListenersRequest), slog.Any("response", describeAppAllListenersResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'appblb.DescribeAppAllListeners': %w", err) } // 获取全部 HTTPS/SSL 监听端口 listeners := make([]struct { Type string Port int32 }, 0) for _, listener := range describeAppAllListenersResp.ListenerList { if listener.ListenerType == "HTTPS" || listener.ListenerType == "SSL" { listeners = append(listeners, struct { Type string Port int32 }{ Type: listener.ListenerType, Port: int32(listener.ListenerPort), }) } } // 遍历更新监听证书 if len(listeners) == 0 { d.logger.Info("no blb listeners to deploy") } else { d.logger.Info("found https/ssl listeners to deploy", slog.Any("listeners", listeners)) var errs []error for _, listener := range listeners { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) updateListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudListenerType string, cloudListenerPort int32, cloudCertId string) error { switch strings.ToUpper(cloudListenerType) { case "HTTPS": return d.updateHttpsListenerCertificate(ctx, cloudLoadbalancerId, cloudListenerPort, cloudCertId) case "SSL": return d.updateSslListenerCertificate(ctx, cloudLoadbalancerId, cloudListenerPort, cloudCertId) default: return fmt.Errorf("unsupported listener type '%s'", cloudListenerType) } } func (d *Deployer) updateHttpsListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudHttpsListenerPort int32, cloudCertId string) error { // 查询 HTTPS 监听器 // REF: https://cloud.baidu.com/doc/BLB/s/ujwvxnyux#describeapphttpslisteners%E6%9F%A5%E8%AF%A2https%E7%9B%91%E5%90%AC%E5%99%A8 describeAppHTTPSListenersReq := &bceappblb.DescribeAppListenerArgs{ ListenerPort: uint16(cloudHttpsListenerPort), MaxKeys: 1, } describeAppHTTPSListenersResp, err := d.sdkClient.DescribeAppHTTPSListeners(cloudLoadbalancerId, describeAppHTTPSListenersReq) d.logger.Debug("sdk request 'appblb.DescribeAppHTTPSListeners'", slog.String("blbId", cloudLoadbalancerId), slog.Any("request", describeAppHTTPSListenersReq), slog.Any("response", describeAppHTTPSListenersResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'appblb.DescribeAppHTTPSListeners': %w", err) } else if len(describeAppHTTPSListenersResp.ListenerList) == 0 { return fmt.Errorf("could not find listener '%s:%d'", cloudLoadbalancerId, cloudHttpsListenerPort) } if d.config.Domain == "" { // 未指定 SNI,只需部署到监听器 // 更新 HTTPS 监听器 // REF: https://cloud.baidu.com/doc/BLB/s/ujwvxnyux#updateapphttpslistener%E6%9B%B4%E6%96%B0https%E7%9B%91%E5%90%AC%E5%99%A8 updateAppHTTPSListenerReq := &bceappblb.UpdateAppHTTPSListenerArgs{ ClientToken: security.RandomString(32), ListenerPort: uint16(cloudHttpsListenerPort), Scheduler: describeAppHTTPSListenersResp.ListenerList[0].Scheduler, CertIds: []string{cloudCertId}, } err := d.sdkClient.UpdateAppHTTPSListener(cloudLoadbalancerId, updateAppHTTPSListenerReq) d.logger.Debug("sdk request 'appblb.UpdateAppHTTPSListener'", slog.Any("request", updateAppHTTPSListenerReq)) if err != nil { return fmt.Errorf("failed to execute sdk request 'appblb.UpdateAppHTTPSListener': %w", err) } } else { // 指定 SNI,需部署到扩展域名 // 更新 HTTPS 监听器 // REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatehttpslistener%E6%9B%B4%E6%96%B0https%E7%9B%91%E5%90%AC%E5%99%A8 updateAppHTTPSListenerReq := &bceappblb.UpdateAppHTTPSListenerArgs{ ClientToken: security.RandomString(32), ListenerPort: uint16(cloudHttpsListenerPort), Scheduler: describeAppHTTPSListenersResp.ListenerList[0].Scheduler, CertIds: describeAppHTTPSListenersResp.ListenerList[0].CertIds, AdditionalCertDomains: lo.Map(describeAppHTTPSListenersResp.ListenerList[0].AdditionalCertDomains, func(domain bceappblb.AdditionalCertDomainsModel, _ int) bceappblb.AdditionalCertDomainsModel { if domain.Host == d.config.Domain { return bceappblb.AdditionalCertDomainsModel{ Host: domain.Host, CertId: cloudCertId, } } return bceappblb.AdditionalCertDomainsModel{ Host: domain.Host, CertId: domain.CertId, } }), } err := d.sdkClient.UpdateAppHTTPSListener(cloudLoadbalancerId, updateAppHTTPSListenerReq) d.logger.Debug("sdk request 'appblb.UpdateAppHTTPSListener'", slog.Any("request", updateAppHTTPSListenerReq)) if err != nil { return fmt.Errorf("failed to execute sdk request 'appblb.UpdateAppHTTPSListener': %w", err) } } return nil } func (d *Deployer) updateSslListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudHttpsListenerPort int32, cloudCertId string) error { // 更新 SSL 监听器 // REF: https://cloud.baidu.com/doc/BLB/s/ujwvxnyux#updateappssllistener%E6%9B%B4%E6%96%B0ssl%E7%9B%91%E5%90%AC%E5%99%A8 updateAppSSLListenerReq := &bceappblb.UpdateAppSSLListenerArgs{ ClientToken: security.RandomString(32), ListenerPort: uint16(cloudHttpsListenerPort), CertIds: []string{cloudCertId}, } err := d.sdkClient.UpdateAppSSLListener(cloudLoadbalancerId, updateAppSSLListenerReq) d.logger.Debug("sdk request 'appblb.UpdateAppSSLListener'", slog.Any("request", updateAppSSLListenerReq)) if err != nil { return fmt.Errorf("failed to execute sdk request 'appblb.UpdateAppSSLListener': %w", err) } return nil } func createSDKClient(accessKeyId, secretAccessKey, region string) (*bceappblb.Client, error) { endpoint := "" if region != "" { endpoint = fmt.Sprintf("blb.%s.baidubce.com", region) } client, err := bceappblb.NewClient(accessKeyId, secretAccessKey, endpoint) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb_test.go ================================================ package baiducloudappblb_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/baiducloud-appblb" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string fRegion string fLoadbalancerId string fDomain string ) func init() { argsPrefix := "BAIDUCLOUDAPPBLB_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./baiducloud_appblb_test.go -args \ --BAIDUCLOUDAPPBLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --BAIDUCLOUDAPPBLB_INPUTKEYPATH="/path/to/your-input-key.pem" \ --BAIDUCLOUDAPPBLB_ACCESSKEYID="your-access-key-id" \ --BAIDUCLOUDAPPBLB_SECRETACCESSKEY="your-secret-access-key" \ --BAIDUCLOUDAPPBLB_REGION="bj" \ --BAIDUCLOUDAPPBLB_LOADBALANCERID="your-blb-loadbalancer-id" \ --BAIDUCLOUDAPPBLB_DOMAIN="your-blb-sni-domain" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, ResourceType: provider.RESOURCE_TYPE_LOADBALANCER, Region: fRegion, LoadbalancerId: fLoadbalancerId, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/baiducloud-appblb/consts.go ================================================ package baiducloudappblb const ( // 资源类型:部署到指定负载均衡器。 RESOURCE_TYPE_LOADBALANCER = "loadbalancer" // 资源类型:部署到指定监听器。 RESOURCE_TYPE_LISTENER = "listener" ) ================================================ FILE: pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go ================================================ package baiducloudblb import ( "context" "errors" "fmt" "log/slog" "strconv" "strings" bceblb "github.com/baidubce/bce-sdk-go/services/blb" "github.com/pocketbase/pocketbase/tools/security" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/baiducloud-cert" "github.com/certimate-go/certimate/pkg/core/deployer" ) type DeployerConfig struct { // 百度智能云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 百度智能云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // 百度智能云区域。 Region string `json:"region"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 负载均衡实例 ID。 // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时必填。 LoadbalancerId string `json:"loadbalancerId,omitempty"` // 负载均衡监听端口。 // 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。 ListenerPort int32 `json:"listenerPort,omitempty"` // SNI 域名(支持泛域名)。 // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时选填。 Domain string `json:"domain,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *bceblb.Client sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, SecretAccessKey: config.SecretAccessKey, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, 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) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_LOADBALANCER: if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil { return nil, err } case RESOURCE_TYPE_LISTENER: if err := d.deployToListener(ctx, upres.CertId); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error { if d.config.LoadbalancerId == "" { return errors.New("config `loadbalancerId` is required") } // 查询 BLB 实例详情 // REF: https://cloud.baidu.com/doc/BLB/s/njwvxnv79#describeloadbalancerdetail%E6%9F%A5%E8%AF%A2blb%E5%AE%9E%E4%BE%8B%E8%AF%A6%E6%83%85 describeLoadBalancerDetailResp, err := d.sdkClient.DescribeLoadBalancerDetail(d.config.LoadbalancerId) d.logger.Debug("sdk request 'blb.DescribeLoadBalancerAttribute'", slog.String("blbId", d.config.LoadbalancerId), slog.Any("response", describeLoadBalancerDetailResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'blb.DescribeLoadBalancerDetail': %w", err) } // 获取全部 HTTPS/SSL 监听端口 listeners := make([]struct { Type string Port int32 }, 0) for _, listener := range describeLoadBalancerDetailResp.Listener { if listener.Type == "HTTPS" || listener.Type == "SSL" { listenerPort, err := strconv.Atoi(listener.Port) if err != nil { continue } listeners = append(listeners, struct { Type string Port int32 }{ Type: listener.Type, Port: int32(listenerPort), }) } } // 遍历更新监听证书 if len(listeners) == 0 { d.logger.Info("no blb listeners to deploy") } else { d.logger.Info("found https/ssl listeners to deploy", slog.Any("listeners", listeners)) var errs []error for _, listener := range listeners { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error { if d.config.LoadbalancerId == "" { return errors.New("config `loadbalancerId` is required") } if d.config.ListenerPort == 0 { return errors.New("config `listenerPort` is required") } // 查询监听 // REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#describealllisteners%E6%9F%A5%E8%AF%A2%E6%89%80%E6%9C%89%E7%9B%91%E5%90%AC describeAllListenersRequest := &bceblb.DescribeListenerArgs{ ListenerPort: uint16(d.config.ListenerPort), } describeAllListenersResp, err := d.sdkClient.DescribeAllListeners(d.config.LoadbalancerId, describeAllListenersRequest) d.logger.Debug("sdk request 'blb.DescribeAllListeners'", slog.String("blbId", d.config.LoadbalancerId), slog.Any("request", describeAllListenersRequest), slog.Any("response", describeAllListenersResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'blb.DescribeAllListeners': %w", err) } // 获取全部 HTTPS/SSL 监听端口 listeners := make([]struct { Type string Port int32 }, 0) for _, listener := range describeAllListenersResp.AllListenerList { if listener.ListenerType == "HTTPS" || listener.ListenerType == "SSL" { listeners = append(listeners, struct { Type string Port int32 }{ Type: listener.ListenerType, Port: int32(listener.ListenerPort), }) } } // 遍历更新监听证书 if len(listeners) == 0 { d.logger.Info("no blb listeners to deploy") } else { d.logger.Info("found https/ssl listeners to deploy", slog.Any("listeners", listeners)) var errs []error for _, listener := range listeners { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) updateListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudListenerType string, cloudListenerPort int32, cloudCertId string) error { switch strings.ToUpper(cloudListenerType) { case "HTTPS": return d.updateHttpsListenerCertificate(ctx, cloudLoadbalancerId, cloudListenerPort, cloudCertId) case "SSL": return d.updateSslListenerCertificate(ctx, cloudLoadbalancerId, cloudListenerPort, cloudCertId) default: return fmt.Errorf("unsupported listener type '%s'", cloudListenerType) } } func (d *Deployer) updateHttpsListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudHttpsListenerPort int32, cloudCertId string) error { // 查询 HTTPS 监听器 // REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#describehttpslisteners%E6%9F%A5%E8%AF%A2https%E7%9B%91%E5%90%AC%E5%99%A8 describeHTTPSListenersReq := &bceblb.DescribeListenerArgs{ ListenerPort: uint16(cloudHttpsListenerPort), MaxKeys: 1, } describeHTTPSListenersResp, err := d.sdkClient.DescribeHTTPSListeners(cloudLoadbalancerId, describeHTTPSListenersReq) d.logger.Debug("sdk request 'blb.DescribeHTTPSListeners'", slog.String("blbId", cloudLoadbalancerId), slog.Any("request", describeHTTPSListenersReq), slog.Any("response", describeHTTPSListenersResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'blb.DescribeHTTPSListeners': %w", err) } else if len(describeHTTPSListenersResp.ListenerList) == 0 { return fmt.Errorf("could not find listener '%s:%d'", cloudLoadbalancerId, cloudHttpsListenerPort) } if d.config.Domain == "" { // 未指定 SNI,只需部署到监听器 // 更新 HTTPS 监听器 // REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatehttpslistener%E6%9B%B4%E6%96%B0https%E7%9B%91%E5%90%AC%E5%99%A8 updateHTTPSListenerReq := &bceblb.UpdateHTTPSListenerArgs{ ClientToken: security.RandomString(32), ListenerPort: uint16(cloudHttpsListenerPort), CertIds: []string{cloudCertId}, } err := d.sdkClient.UpdateHTTPSListener(cloudLoadbalancerId, updateHTTPSListenerReq) d.logger.Debug("sdk request 'blb.UpdateHTTPSListener'", slog.Any("request", updateHTTPSListenerReq)) if err != nil { return fmt.Errorf("failed to execute sdk request 'blb.UpdateHTTPSListener': %w", err) } } else { // 指定 SNI,需部署到扩展域名 // 更新 HTTPS 监听器 // REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatehttpslistener%E6%9B%B4%E6%96%B0https%E7%9B%91%E5%90%AC%E5%99%A8 updateHTTPSListenerReq := &bceblb.UpdateHTTPSListenerArgs{ ClientToken: security.RandomString(32), ListenerPort: uint16(cloudHttpsListenerPort), CertIds: describeHTTPSListenersResp.ListenerList[0].CertIds, AdditionalCertDomains: lo.Map(describeHTTPSListenersResp.ListenerList[0].AdditionalCertDomains, func(domain bceblb.AdditionalCertDomainsModel, _ int) bceblb.AdditionalCertDomainsModel { if domain.Host == d.config.Domain { return bceblb.AdditionalCertDomainsModel{ Host: domain.Host, CertId: cloudCertId, } } return bceblb.AdditionalCertDomainsModel{ Host: domain.Host, CertId: domain.CertId, } }), } err := d.sdkClient.UpdateHTTPSListener(cloudLoadbalancerId, updateHTTPSListenerReq) d.logger.Debug("sdk request 'blb.UpdateHTTPSListener'", slog.Any("request", updateHTTPSListenerReq)) if err != nil { return fmt.Errorf("failed to execute sdk request 'blb.UpdateHTTPSListener': %w", err) } } return nil } func (d *Deployer) updateSslListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudHttpsListenerPort int32, cloudCertId string) error { // 更新 SSL 监听器 // REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatessllistener%E6%9B%B4%E6%96%B0ssl%E7%9B%91%E5%90%AC%E5%99%A8 updateSSLListenerReq := &bceblb.UpdateSSLListenerArgs{ ClientToken: security.RandomString(32), ListenerPort: uint16(cloudHttpsListenerPort), CertIds: []string{cloudCertId}, } err := d.sdkClient.UpdateSSLListener(cloudLoadbalancerId, updateSSLListenerReq) d.logger.Debug("sdk request 'blb.UpdateSSLListener'", slog.Any("request", updateSSLListenerReq)) if err != nil { return fmt.Errorf("failed to execute sdk request 'blb.UpdateSSLListener': %w", err) } return nil } func createSDKClient(accessKeyId, secretAccessKey, region string) (*bceblb.Client, error) { endpoint := "" if region != "" { endpoint = fmt.Sprintf("blb.%s.baidubce.com", region) } client, err := bceblb.NewClient(accessKeyId, secretAccessKey, endpoint) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb_test.go ================================================ package baiducloudblb_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/baiducloud-blb" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string fRegion string fLoadbalancerId string fDomain string ) func init() { argsPrefix := "BAIDUCLOUDBLB_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./baiducloud_blb_test.go -args \ --BAIDUCLOUDBLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --BAIDUCLOUDBLB_INPUTKEYPATH="/path/to/your-input-key.pem" \ --BAIDUCLOUDBLB_ACCESSKEYID="your-access-key-id" \ --BAIDUCLOUDBLB_SECRETACCESSKEY="your-secret-access-key" \ --BAIDUCLOUDBLB_REGION="bj" \ --BAIDUCLOUDBLB_LOADBALANCERID="your-blb-loadbalancer-id" \ --BAIDUCLOUDBLB_DOMAIN="your-blb-sni-domain" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, ResourceType: provider.RESOURCE_TYPE_LOADBALANCER, Region: fRegion, LoadbalancerId: fLoadbalancerId, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/baiducloud-blb/consts.go ================================================ package baiducloudblb const ( // 资源类型:部署到指定负载均衡器。 RESOURCE_TYPE_LOADBALANCER = "loadbalancer" // 资源类型:部署到指定监听器。 RESOURCE_TYPE_LISTENER = "listener" ) ================================================ FILE: pkg/core/deployer/providers/baiducloud-cdn/baiducloud_cdn.go ================================================ package baiducloudcdn import ( "context" "errors" "fmt" "log/slog" "strings" "time" bcecdn "github.com/baidubce/bce-sdk-go/services/cdn" bcecdnapi "github.com/baidubce/bce-sdk-go/services/cdn/api" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/samber/lo" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 百度智能云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 百度智能云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *bcecdn.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey) 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) (*deployer.DeployResult, error) { // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return xcerthostname.IsMatch(d.config.Domain, domain) }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by wildcard") } } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no cdn domains to deploy") } else { d.logger.Info("found cdn domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, certPEM, privkeyPEM); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 查询域名列表 // REF: https://cloud.baidu.com/doc/CDN/s/sjwvyewt1 listDomainsMarker := "" for { select { case <-ctx.Done(): return nil, ctx.Err() default: } listDomainsRespDomains, listDomainsNextMarker, err := d.sdkClient.ListDomains(listDomainsMarker) d.logger.Debug("sdk request 'cdn.ListDomains'", slog.String("request.marker", listDomainsMarker), slog.Any("response.domains", listDomainsRespDomains)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.ListDomains': %w", err) } domains = append(domains, listDomainsRespDomains...) if listDomainsNextMarker == "" { break } listDomainsMarker = listDomainsNextMarker } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, certPEM, privkeyPEM string) error { // 修改域名证书 // REF: https://cloud.baidu.com/doc/CDN/s/qjzuz2hp8 putCertResp, err := d.sdkClient.PutCert( domain, &bcecdnapi.UserCertificate{ CertName: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()), ServerData: certPEM, PrivateData: privkeyPEM, }, "ON", ) d.logger.Debug("sdk request 'cdn.PutCert'", slog.String("request.domain", domain), slog.Any("response", putCertResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'cdn.PutCert': %w", err) } return nil } func createSDKClient(accessKeyId, secretAccessKey string) (*bcecdn.Client, error) { client, err := bcecdn.NewClient(accessKeyId, secretAccessKey, "") if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/baiducloud-cdn/baiducloud_cdn_test.go ================================================ package baiducloudcdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/baiducloud-cdn" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string fDomain string ) func init() { argsPrefix := "BAIDUCLOUDCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./baiducloud_cdn_test.go -args \ --BAIDUCLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --BAIDUCLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --BAIDUCLOUDCDN_ACCESSKEYID="your-access-key-id" \ --BAIDUCLOUDCDN_SECRETACCESSKEY="your-secret-access-key" \ --BAIDUCLOUDCDN_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/baiducloud-cdn/consts.go ================================================ package baiducloudcdn const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/baiducloud-cert/baiducloud_cert.go ================================================ package baiducloudcert import ( "context" "errors" "fmt" "log/slog" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/baiducloud-cert" "github.com/certimate-go/certimate/pkg/core/deployer" ) type DeployerConfig struct { // 百度智能云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 百度智能云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, SecretAccessKey: config.SecretAccessKey, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkCertmgr: pcertmgr, }, 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) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } return &deployer.DeployResult{}, nil } ================================================ FILE: pkg/core/deployer/providers/baishan-cdn/baishan_cdn.go ================================================ package baishancdn import ( "context" "encoding/json" "errors" "fmt" "log/slog" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/baishan-cdn" "github.com/certimate-go/certimate/pkg/core/deployer" baishansdk "github.com/certimate-go/certimate/pkg/sdk3rd/baishan" ) type DeployerConfig struct { // 白山云 API Token。 ApiToken string `json:"apiToken"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 域名匹配模式。暂时只支持精确匹配。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` // 证书 ID。 // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。 CertificateId string `json:"certificateId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *baishansdk.Client sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("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) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ ApiToken: config.ApiToken, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, 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) (*deployer.DeployResult, error) { // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_DOMAIN: if err := d.deployToDomain(ctx, certPEM, privkeyPEM); err != nil { return nil, err } case RESOURCE_TYPE_CERTIFICATE: if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToDomain(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.Domain == "" { return errors.New("config `domain` is required") } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 查询域名配置 // REF: https://portal.baishancloud.com/track/document/api/1/1065 getDomainConfigReq := &baishansdk.GetDomainConfigRequest{ Domains: lo.ToPtr(d.config.Domain), Config: lo.ToPtr([]string{"https"}), } getDomainConfigResp, err := d.sdkClient.GetDomainConfigWithContext(ctx, getDomainConfigReq) d.logger.Debug("sdk request 'baishan.GetDomainConfig'", slog.Any("request", getDomainConfigReq), slog.Any("response", getDomainConfigResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'baishan.GetDomainConfig': %w", err) } else if len(getDomainConfigResp.Data) == 0 { return fmt.Errorf("could not find domain '%s'", d.config.Domain) } // 设置域名配置 // REF: https://portal.baishancloud.com/track/document/api/1/1045 setDomainConfigReq := &baishansdk.SetDomainConfigRequest{ Domains: lo.ToPtr(d.config.Domain), Config: &baishansdk.DomainConfig{ Https: &baishansdk.DomainConfigHttps{ CertId: json.Number(upres.CertId), ForceHttps: getDomainConfigResp.Data[0].Config.Https.ForceHttps, EnableHttp2: getDomainConfigResp.Data[0].Config.Https.EnableHttp2, EnableOcsp: getDomainConfigResp.Data[0].Config.Https.EnableOcsp, }, }, } setDomainConfigResp, err := d.sdkClient.SetDomainConfigWithContext(ctx, setDomainConfigReq) d.logger.Debug("sdk request 'baishan.SetDomainConfig'", slog.Any("request", setDomainConfigReq), slog.Any("response", setDomainConfigResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'baishan.SetDomainConfig': %w", err) } return nil } func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.CertificateId == "" { return errors.New("config `certificateId` is required") } // 替换证书 opres, err := d.sdkCertmgr.Replace(ctx, d.config.CertificateId, certPEM, privkeyPEM) if err != nil { return fmt.Errorf("failed to replace certificate file: %w", err) } else { d.logger.Info("ssl certificate replaced", slog.Any("result", opres)) } return nil } func createSDKClient(apiToken string) (*baishansdk.Client, error) { return baishansdk.NewClient(apiToken) } ================================================ FILE: pkg/core/deployer/providers/baishan-cdn/baishan_cdn_test.go ================================================ package baishancdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/baishan-cdn" ) var ( fInputCertPath string fInputKeyPath string fApiToken string fDomain string ) func init() { argsPrefix := "BAISHANCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./baishan_cdn_test.go -args \ --BAISHANCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --BAISHANCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --BAISHANCDN_APITOKEN="your-api-token" \ --BAISHANCDN_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("APITOKEN: %v", fApiToken), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ApiToken: fApiToken, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/baishan-cdn/consts.go ================================================ package baishancdn const ( // 资源类型:替换指定域名的证书。 RESOURCE_TYPE_DOMAIN = "domain" // 资源类型:替换指定证书。 RESOURCE_TYPE_CERTIFICATE = "certificate" ) const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" ) ================================================ FILE: pkg/core/deployer/providers/baotapanel/baotapanel.go ================================================ package baotapanel import ( "context" "crypto/tls" "errors" "fmt" "log/slog" "time" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/deployer" btsdk "github.com/certimate-go/certimate/pkg/sdk3rd/btpanel" xwait "github.com/certimate-go/certimate/pkg/utils/wait" ) type DeployerConfig struct { // 宝塔面板服务地址。 ServerUrl string `json:"serverUrl"` // 宝塔面板接口密钥。 ApiKey string `json:"apiKey"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` // 网站类型。 SiteType string `json:"siteType"` // 网站名称。 SiteNames []string `json:"siteNames,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *btsdk.Client } var _ deployer.Provider = (*Deployer)(nil) var btProjectTypes = []string{"php", "java", "nodejs", "go", "python", "proxy", "html", "general"} func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections) 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) (*deployer.DeployResult, error) { if len(d.config.SiteNames) == 0 { return nil, errors.New("config `siteNames` is required") } switch d.config.SiteType { case "any": { // 上传证书 sslCertSaveCertReq := &btsdk.SSLCertSaveCertRequest{ Certificate: certPEM, PrivateKey: privkeyPEM, } sslCertSaveCertResp, err := d.sdkClient.SSLCertSaveCertWithContext(ctx, sslCertSaveCertReq) d.logger.Debug("sdk request 'bt.SSLCertSaveCert'", slog.Any("request", sslCertSaveCertReq), slog.Any("response", sslCertSaveCertResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'bt.SSLCertSaveCert': %w", err) } // 设置站点证书 sslSetBatchCertToSiteReq := &btsdk.SSLSetBatchCertToSiteRequest{ BatchInfo: lo.Map(d.config.SiteNames, func(siteName string, _ int) *btsdk.SSLSetBatchCertToSiteRequestBatchInfo { return &btsdk.SSLSetBatchCertToSiteRequestBatchInfo{ SiteName: siteName, SSLHash: sslCertSaveCertResp.SSLHash, } }), } sslSetBatchCertToSiteResp, err := d.sdkClient.SSLSetBatchCertToSiteWithContext(ctx, sslSetBatchCertToSiteReq) d.logger.Debug("sdk request 'bt.SSLSetBatchCertToSite'", slog.Any("request", sslSetBatchCertToSiteReq), slog.Any("response", sslSetBatchCertToSiteResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'bt.SSLSetBatchCertToSite': %w", err) } } default: { if d.config.SiteType != "" { if !lo.Contains(btProjectTypes, d.config.SiteType) { return nil, fmt.Errorf("unsupported site type: '%s'", d.config.SiteType) } } // 遍历更新站点证书 var errs []error for i, siteName := range d.config.SiteNames { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateSiteCertificate(ctx, d.config.SiteType, siteName, certPEM, privkeyPEM); err != nil { errs = append(errs, err) } if i < len(d.config.SiteNames)-1 { xwait.DelayWithContext(ctx, time.Second*5) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } } return &deployer.DeployResult{}, nil } func (d *Deployer) updateSiteCertificate(ctx context.Context, siteType, siteName string, certPEM, privkeyPEM string) error { switch siteType { case "proxy": { // 设置代理 SSL 证书 modProxyComSetSSLReq := &btsdk.ModProxyComSetSSLRequest{ SiteName: siteName, Certificate: certPEM, PrivateKey: privkeyPEM, } modProxyComSetSSLResp, err := d.sdkClient.ModProxyComSetSSLWithContext(ctx, modProxyComSetSSLReq) d.logger.Debug("sdk request 'bt.ModProxyComSetSSL'", slog.Any("request", modProxyComSetSSLReq), slog.Any("response", modProxyComSetSSLResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'bt.ModProxyComSetSSL': %w", err) } } default: { // 设置站点 SSL 证书 siteSetSSLReq := &btsdk.SiteSetSSLRequest{ Type: "0", SiteName: siteName, Certificate: certPEM, PrivateKey: privkeyPEM, } siteSetSSLResp, err := d.sdkClient.SiteSetSSLWithContext(ctx, siteSetSSLReq) d.logger.Debug("sdk request 'bt.SiteSetSSL'", slog.Any("request", siteSetSSLReq), slog.Any("response", siteSetSSLResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'bt.SiteSetSSL': %w", err) } } } return nil } func createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*btsdk.Client, error) { client, err := btsdk.NewClient(serverUrl, apiKey) if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } ================================================ FILE: pkg/core/deployer/providers/baotapanel/baotapanel_test.go ================================================ package baotapanel_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/baotapanel" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fApiKey string fSiteType string fSiteName string ) func init() { argsPrefix := "BAOTAPANEL_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") flag.StringVar(&fSiteType, argsPrefix+"SITETYPE", "", "") flag.StringVar(&fSiteName, argsPrefix+"SITENAME", "", "") } /* Shell command to run this test: go test -v ./baotapanel_test.go -args \ --BAOTAPANEL_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --BAOTAPANEL_INPUTKEYPATH="/path/to/your-input-key.pem" \ --BAOTAPANEL_SERVERURL="http://127.0.0.1:8888" \ --BAOTAPANEL_APIKEY="your-api-key" \ --BAOTAPANEL_SITETYPE="php" \ --BAOTAPANEL_SITENAME="your-site-name" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("APIKEY: %v", fApiKey), fmt.Sprintf("SITETYPE: %v", fSiteType), fmt.Sprintf("SITENAME: %v", fSiteName), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ServerUrl: fServerUrl, ApiKey: fApiKey, AllowInsecureConnections: true, SiteType: fSiteType, SiteNames: []string{fSiteName}, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/baotapanel-console/baotapanel_console.go ================================================ package baotapanelconsole import ( "context" "crypto/tls" "errors" "fmt" "log/slog" "github.com/certimate-go/certimate/pkg/core/deployer" btsdk "github.com/certimate-go/certimate/pkg/sdk3rd/btpanel" ) type DeployerConfig struct { // 宝塔面板服务地址。 ServerUrl string `json:"serverUrl"` // 宝塔面板接口密钥。 ApiKey string `json:"apiKey"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` // 是否自动重启。 AutoRestart bool `json:"autoRestart"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *btsdk.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections) 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) (*deployer.DeployResult, error) { // 设置面板 SSL 证书 configSavePanelSSLReq := &btsdk.ConfigSavePanelSSLRequest{ PrivateKey: privkeyPEM, Certificate: certPEM, } configSavePanelSSLResp, err := d.sdkClient.ConfigSavePanelSSLWithContext(ctx, configSavePanelSSLReq) d.logger.Debug("sdk request 'bt.ConfigSavePanelSSL'", slog.Any("request", configSavePanelSSLReq), slog.Any("response", configSavePanelSSLResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'bt.ConfigSavePanelSSL': %w", err) } if d.config.AutoRestart { // 重启面板(无需关心响应,因为宝塔重启时会断开连接产生 error) systemServiceAdminReq := &btsdk.SystemServiceAdminRequest{ Name: "nginx", Type: "restart", } systemServiceAdminResp, _ := d.sdkClient.SystemServiceAdminWithContext(ctx, systemServiceAdminReq) d.logger.Debug("sdk request 'bt.SystemServiceAdmin'", slog.Any("request", systemServiceAdminReq), slog.Any("response", systemServiceAdminResp)) } return &deployer.DeployResult{}, nil } func createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*btsdk.Client, error) { client, err := btsdk.NewClient(serverUrl, apiKey) if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } ================================================ FILE: pkg/core/deployer/providers/baotapanel-console/baotapanel_console_test.go ================================================ package baotapanelconsole_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/baotapanel-console" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fApiKey string ) func init() { argsPrefix := "BAOTAPANELCONSOLE_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") } /* Shell command to run this test: go test -v ./baotapanel_console_test.go -args \ --BAOTAPANELCONSOLE_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --BAOTAPANELCONSOLE_INPUTKEYPATH="/path/to/your-input-key.pem" \ --BAOTAPANELCONSOLE_SERVERURL="http://127.0.0.1:8888" \ --BAOTAPANELCONSOLE_APIKEY="your-api-key" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("APIKEY: %v", fApiKey), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ServerUrl: fServerUrl, ApiKey: fApiKey, AllowInsecureConnections: true, AutoRestart: true, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/baotapanelgo/baotapanelgo.go ================================================ package baotapanelgo import ( "context" "crypto/sha256" "crypto/tls" "encoding/hex" "errors" "fmt" "log/slog" "strings" "time" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/deployer" btsdk "github.com/certimate-go/certimate/pkg/sdk3rd/btpanelgo" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xwait "github.com/certimate-go/certimate/pkg/utils/wait" ) type DeployerConfig struct { // 宝塔面板服务地址。 ServerUrl string `json:"serverUrl"` // 宝塔面板接口密钥。 ApiKey string `json:"apiKey"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` // 网站类型。 SiteType string `json:"siteType"` // 网站名称。 SiteNames []string `json:"siteNames,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *btsdk.Client } var _ deployer.Provider = (*Deployer)(nil) var ( btProjectTypes = []string{"php", "java", "asp", "go", "python", "nodejs", "proxy", "general"} btProjectTypesInIIS = []string{"php", "asp", "aspx"} ) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections) 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) (*deployer.DeployResult, error) { if len(d.config.SiteNames) == 0 { return nil, errors.New("config `siteNames` is required") } if d.config.SiteType != "" { if !lo.Contains(btProjectTypes, d.config.SiteType) && !lo.Contains(btProjectTypesInIIS, d.config.SiteType) { return nil, fmt.Errorf("unsupported site type: '%s'", d.config.SiteType) } } // 遍历更新站点证书 var errs []error for i, siteName := range d.config.SiteNames { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateSiteCertificate(ctx, d.config.SiteType, siteName, certPEM, privkeyPEM); err != nil { errs = append(errs, err) } if i < len(d.config.SiteNames)-1 { xwait.DelayWithContext(ctx, time.Second*5) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } return &deployer.DeployResult{}, nil } func (d *Deployer) findSiteByName(ctx context.Context, siteType, siteName string) (*btsdk.SiteData, error) { if siteType == "" || lo.Contains(btProjectTypesInIIS, siteType) { // 查询网站列表 datalistGetDataListPage := 1 datalistGetDataListLimit := 10 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } datalistGetDataListReq := &btsdk.DatalistGetDataListRequest{ Table: lo.ToPtr("sites"), SearchString: lo.ToPtr(siteName), Page: lo.ToPtr(int32(datalistGetDataListPage)), Limit: lo.ToPtr(int32(datalistGetDataListLimit)), } datalistGetDataListResp, err := d.sdkClient.DatalistGetDataListWithContext(ctx, datalistGetDataListReq) d.logger.Debug("sdk request 'bt.DatalistGetDataList'", slog.Any("request", datalistGetDataListReq), slog.Any("response", datalistGetDataListResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'bt.DatalistGetDataList': %w", err) } for _, siteItem := range datalistGetDataListResp.Data { if strings.EqualFold(siteItem.Name, siteName) { return siteItem, nil } } if len(datalistGetDataListResp.Data) < datalistGetDataListLimit { break } datalistGetDataListPage++ } } else { // 查询网站列表 siteGetProjectListPage := 1 siteGetProjectListLimit := 10 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } siteGetProjectListReq := &btsdk.SiteGetProjectListRequest{ SearchType: lo.ToPtr(siteType), SearchString: lo.ToPtr(siteName), Page: lo.ToPtr(int32(siteGetProjectListPage)), Limit: lo.ToPtr(int32(siteGetProjectListLimit)), } siteGetProjectListResp, err := d.sdkClient.SiteGetProjectListWithContext(ctx, siteGetProjectListReq) d.logger.Debug("sdk request 'bt.SiteGetProjectList'", slog.Any("request", siteGetProjectListReq), slog.Any("response", siteGetProjectListResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'bt.SiteGetProjectList': %w", err) } for _, siteItem := range siteGetProjectListResp.Data { if strings.EqualFold(siteItem.Name, siteName) { return siteItem, nil } } if len(siteGetProjectListResp.Data) < siteGetProjectListLimit { break } siteGetProjectListPage++ } } return nil, fmt.Errorf("could not find site '%s'", siteName) } func (d *Deployer) updateSiteCertificate(ctx context.Context, siteType, siteName string, certPEM, privkeyPEM string) error { // 获取面板配置 panelGetConfigReq := &btsdk.PanelGetConfigRequest{} panelGetConfigResp, err := d.sdkClient.PanelGetConfigWithContext(ctx, panelGetConfigReq) d.logger.Debug("sdk request 'bt.PanelGetConfig'", slog.Any("request", panelGetConfigReq), slog.Any("response", panelGetConfigResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'bt.PanelGetConfig': %w", err) } // 获取网站 siteData, err := d.findSiteByName(ctx, siteType, siteName) if err != nil { return err } // 根据服务器类型部署证书 pfxRequried := panelGetConfigResp.Site != nil && strings.EqualFold(panelGetConfigResp.Site.WebServer, "iis") if pfxRequried { // 转换证书格式 certPFXPassword := "certimate" certPFX, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, certPFXPassword) if err != nil { return fmt.Errorf("failed to transform certificate from PEM to PFX: %w", err) } // 上传证书 certPFXHash := sha256.Sum256([]byte(certPFX)) certPFXHashHex := hex.EncodeToString(certPFXHash[:]) certPFXPath := panelGetConfigResp.Paths.Soft + "/temp/ssl/certimate" certPFXFileName := fmt.Sprintf("%s.pfx", certPFXHashHex) filesUploadReq := &btsdk.FilesUploadRequest{ Path: lo.ToPtr(certPFXPath), Name: lo.ToPtr(certPFXFileName), Start: lo.ToPtr(int32(0)), Size: lo.ToPtr(int32(len(certPFX))), Blob: certPFX, Force: lo.ToPtr(true), } filesUploadResp, err := d.sdkClient.FilesUploadWithContext(ctx, filesUploadReq) d.logger.Debug("sdk request 'bt.FilesUpload'", slog.Any("request", filesUploadReq), slog.Any("response", filesUploadResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'bt.FilesUpload': %w", err) } // 服务器为 IIS,设置网站 SSL siteSetSitePFXSSLReq := &btsdk.SiteSetSitePFXSSLRequest{ SiteId: lo.ToPtr(siteData.Id), PFX: lo.ToPtr(fmt.Sprintf("%s/%s", certPFXPath, certPFXFileName)), Password: lo.ToPtr(certPFXPassword), } siteSetSitePFXSSLResp, err := d.sdkClient.SiteSetSitePFXSSLWithContext(ctx, siteSetSitePFXSSLReq) d.logger.Debug("sdk request 'bt.SiteSetSitePFXSSL'", slog.Any("request", siteSetSitePFXSSLReq), slog.Any("response", siteSetSitePFXSSLResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'bt.SiteSetSitePFXSSL': %w", err) } } else { // 服务器非 IIS,设置网站 SSL siteSetSiteSSLReq := &btsdk.SiteSetSiteSSLRequest{ SiteId: lo.ToPtr(siteData.Id), Status: lo.ToPtr(true), Key: lo.ToPtr(privkeyPEM), Cert: lo.ToPtr(certPEM), } siteSetSiteSSLResp, err := d.sdkClient.SiteSetSiteSSLWithContext(ctx, siteSetSiteSSLReq) d.logger.Debug("sdk request 'bt.SiteSetSiteSSL'", slog.Any("request", siteSetSiteSSLReq), slog.Any("response", siteSetSiteSSLResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'bt.SiteSetSiteSSL': %w", err) } } return nil } func createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*btsdk.Client, error) { client, err := btsdk.NewClient(serverUrl, apiKey) if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } ================================================ FILE: pkg/core/deployer/providers/baotapanelgo/baotapanelgo_test.go ================================================ package baotapanelgo_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/baotapanelgo" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fApiKey string fSiteType string fSiteName string ) func init() { argsPrefix := "BAOTAPANELGO_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") flag.StringVar(&fSiteType, argsPrefix+"SITETYPE", "", "") flag.StringVar(&fSiteName, argsPrefix+"SITENAME", "", "") } /* Shell command to run this test: go test -v ./baotapanelgo_test.go -args \ --BAOTAPANELGO_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --BAOTAPANELGO_INPUTKEYPATH="/path/to/your-input-key.pem" \ --BAOTAPANELGO_SERVERURL="http://127.0.0.1:8888" \ --BAOTAPANELGO_APIKEY="your-api-key" \ --BAOTAPANELGO_SITETYPE="your-site-type" \ --BAOTAPANELGO_SITENAME="your-site-name" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("APIKEY: %v", fApiKey), fmt.Sprintf("SITETYPE: %v", fSiteType), fmt.Sprintf("SITENAME: %v", fSiteName), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ServerUrl: fServerUrl, ApiKey: fApiKey, AllowInsecureConnections: true, SiteType: fSiteType, SiteNames: []string{fSiteName}, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/baotapanelgo-console/baotapanelgo_console.go ================================================ package baotapanelgoconsole import ( "context" "crypto/tls" "errors" "fmt" "log/slog" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/deployer" btsdk "github.com/certimate-go/certimate/pkg/sdk3rd/btpanelgo" ) type DeployerConfig struct { // 宝塔面板服务地址。 ServerUrl string `json:"serverUrl"` // 宝塔面板接口密钥。 ApiKey string `json:"apiKey"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *btsdk.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections) 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) (*deployer.DeployResult, error) { // 设置面板 SSL 证书 configSetPanelSSLReq := &btsdk.ConfigSetPanelSSLRequest{ SSLStatus: lo.ToPtr(int32(1)), SSLKey: lo.ToPtr(privkeyPEM), SSLPem: lo.ToPtr(certPEM), } configSetPanelSSLResp, err := d.sdkClient.ConfigSetPanelSSLWithContext(ctx, configSetPanelSSLReq) d.logger.Debug("sdk request 'bt.ConfigSetPanelSSL'", slog.Any("request", configSetPanelSSLReq), slog.Any("response", configSetPanelSSLResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'bt.ConfigSetPanelSSL': %w", err) } return &deployer.DeployResult{}, nil } func createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*btsdk.Client, error) { client, err := btsdk.NewClient(serverUrl, apiKey) if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } ================================================ FILE: pkg/core/deployer/providers/baotapanelgo-console/baotapanelgo_console_test.go ================================================ package baotapanelgoconsole_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/baotapanelgo-console" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fApiKey string ) func init() { argsPrefix := "BAOTAPANELGOCONSOLE_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") } /* Shell command to run this test: go test -v ./baotapanelgo_console_test.go -args \ --BAOTAPANELGOCONSOLE_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --BAOTAPANELGOCONSOLE_INPUTKEYPATH="/path/to/your-input-key.pem" \ --BAOTAPANELGOCONSOLE_SERVERURL="http://127.0.0.1:8888" \ --BAOTAPANELGOCONSOLE_APIKEY="your-api-key" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("APIKEY: %v", fApiKey), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ServerUrl: fServerUrl, ApiKey: fApiKey, AllowInsecureConnections: true, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/baotawaf/baotawaf.go ================================================ package baotawaf import ( "context" "crypto/tls" "errors" "fmt" "log/slog" "time" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/deployer" btwafsdk "github.com/certimate-go/certimate/pkg/sdk3rd/btwaf" xwait "github.com/certimate-go/certimate/pkg/utils/wait" ) type DeployerConfig struct { // 堡塔云 WAF 服务地址。 ServerUrl string `json:"serverUrl"` // 堡塔云 WAF 接口密钥。 ApiKey string `json:"apiKey"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` // 网站名称。 SiteNames []string `json:"siteNames"` // 网站 SSL 端口。 // 零值时默认值 443。 SitePort int32 `json:"sitePort,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *btwafsdk.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections) 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) (*deployer.DeployResult, error) { if len(d.config.SiteNames) == 0 { return nil, errors.New("config `siteNames` is required") } // 遍历更新站点证书 var errs []error for i, siteName := range d.config.SiteNames { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateSiteCertificate(ctx, siteName, d.config.SitePort, certPEM, privkeyPEM); err != nil { errs = append(errs, err) } if i < len(d.config.SiteNames)-1 { xwait.DelayWithContext(ctx, time.Second*5) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } return &deployer.DeployResult{}, nil } func (d *Deployer) findSiteByName(ctx context.Context, siteName string) (*btwafsdk.SiteRecord, error) { // 查询网站列表 getSiteListPage := 1 getSiteListPageSize := 100 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } getSiteListReq := &btwafsdk.GetSiteListRequest{ SiteName: lo.ToPtr(siteName), Page: lo.ToPtr(int32(getSiteListPage)), PageSize: lo.ToPtr(int32(getSiteListPageSize)), } getSiteListResp, err := d.sdkClient.GetSiteListWithContext(ctx, getSiteListReq) d.logger.Debug("sdk request 'bt.GetSiteList'", slog.Any("request", getSiteListReq), slog.Any("response", getSiteListResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'bt.GetSiteList': %w", err) } if getSiteListResp.Result == nil { break } for _, siteItem := range getSiteListResp.Result.List { if siteItem.SiteName == siteName { return siteItem, nil } } if len(getSiteListResp.Result.List) < getSiteListPageSize { break } getSiteListPage++ } return nil, fmt.Errorf("could not find site '%s'", siteName) } func (d *Deployer) updateSiteCertificate(ctx context.Context, siteName string, sitePort int32, certPEM, privkeyPEM string) error { if sitePort == 0 { sitePort = 443 } // 获取网站配置 siteData, err := d.findSiteByName(ctx, siteName) if err != nil { return err } // 修改网站配置 modifySiteReq := &btwafsdk.ModifySiteRequest{ SiteId: lo.ToPtr(siteData.SiteId), Type: lo.ToPtr("openCert"), Server: &btwafsdk.SiteServerInfoMod{ ListenSSLPorts: lo.ToPtr([]string{fmt.Sprintf("%d", d.config.SitePort)}), SSL: &btwafsdk.SiteServerSSLInfo{ IsSSL: lo.ToPtr(int32(1)), FullChain: lo.ToPtr(certPEM), PrivateKey: lo.ToPtr(privkeyPEM), }, }, } modifySiteResp, err := d.sdkClient.ModifySiteWithContext(ctx, modifySiteReq) d.logger.Debug("sdk request 'bt.ModifySite'", slog.Any("request", modifySiteReq), slog.Any("response", modifySiteResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'bt.ModifySite': %w", err) } return nil } func createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*btwafsdk.Client, error) { client, err := btwafsdk.NewClient(serverUrl, apiKey) if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } ================================================ FILE: pkg/core/deployer/providers/baotawaf/baotawaf_test.go ================================================ package baotawaf_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/baotawaf" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fApiKey string fSiteName string fSitePort int64 ) func init() { argsPrefix := "BAOTAWAF_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") flag.StringVar(&fSiteName, argsPrefix+"SITENAME", "", "") flag.Int64Var(&fSitePort, argsPrefix+"SITEPORT", 0, "") } /* Shell command to run this test: go test -v ./baotawaf_test.go -args \ --BAOTAWAF_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --BAOTAWAF_INPUTKEYPATH="/path/to/your-input-key.pem" \ --BAOTAWAF_SERVERURL="http://127.0.0.1:8888" \ --BAOTAWAF_APIKEY="your-api-key" \ --BAOTAWAF_SITENAME="your-site-name" \ --BAOTAWAF_SITEPORT=443 */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("APIKEY: %v", fApiKey), fmt.Sprintf("SITENAME: %v", fSiteName), fmt.Sprintf("SITEPORT: %v", fSitePort), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ServerUrl: fServerUrl, ApiKey: fApiKey, AllowInsecureConnections: true, SiteNames: []string{fSiteName}, SitePort: int32(fSitePort), }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/baotawaf-console/baotawaf_console.go ================================================ package baotapanelconsole import ( "context" "crypto/tls" "errors" "fmt" "log/slog" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/deployer" btwafsdk "github.com/certimate-go/certimate/pkg/sdk3rd/btwaf" ) type DeployerConfig struct { // 堡塔云 WAF 服务地址。 ServerUrl string `json:"serverUrl"` // 堡塔云 WAF 接口密钥。 ApiKey string `json:"apiKey"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *btwafsdk.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections) 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) (*deployer.DeployResult, error) { // 设置面板 SSL configSetCertReq := &btwafsdk.ConfigSetCertRequest{ CertContent: lo.ToPtr(certPEM), KeyContent: lo.ToPtr(privkeyPEM), } configSetCertResp, err := d.sdkClient.ConfigSetCertWithContext(ctx, configSetCertReq) d.logger.Debug("sdk request 'bt.ConfigSetCert'", slog.Any("request", configSetCertReq), slog.Any("response", configSetCertResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'bt.ConfigSetCert': %w", err) } return &deployer.DeployResult{}, nil } func createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*btwafsdk.Client, error) { client, err := btwafsdk.NewClient(serverUrl, apiKey) if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } ================================================ FILE: pkg/core/deployer/providers/baotawaf-console/baotawaf_console_test.go ================================================ package baotapanelconsole_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/baotawaf-console" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fApiKey string fSiteName string fSitePort int64 ) func init() { argsPrefix := "BAOTAWAFCONSOLE_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") } /* Shell command to run this test: go test -v ./baotawaf_console_test.go -args \ --BAOTAWAFCONSOLE_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --BAOTAWAFCONSOLE_INPUTKEYPATH="/path/to/your-input-key.pem" \ --BAOTAWAFCONSOLE_SERVERURL="http://127.0.0.1:8888" \ --BAOTAWAFCONSOLE_APIKEY="your-api-key" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("APIKEY: %v", fApiKey), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ServerUrl: fServerUrl, ApiKey: fApiKey, AllowInsecureConnections: true, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/bunny-cdn/bunny_cdn.go ================================================ package bunnycdn import ( "context" "encoding/base64" "errors" "fmt" "log/slog" "github.com/certimate-go/certimate/pkg/core/deployer" bunnysdk "github.com/certimate-go/certimate/pkg/sdk3rd/bunny" ) type DeployerConfig struct { // Bunny API Key。 ApiKey string `json:"apiKey"` // Bunny Pull Zone ID。 PullZoneId string `json:"pullZoneId"` // Bunny CDN Hostname。 Hostname string `json:"hostname"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *bunnysdk.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ApiKey) 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) (*deployer.DeployResult, error) { if d.config.PullZoneId == "" { return nil, fmt.Errorf("config `pullZoneId` is required") } if d.config.Hostname == "" { return nil, fmt.Errorf("config `hostname` is required") } // 上传证书 createCertificateReq := &bunnysdk.AddCustomCertificateRequest{ Hostname: d.config.Hostname, Certificate: base64.StdEncoding.EncodeToString([]byte(certPEM)), CertificateKey: base64.StdEncoding.EncodeToString([]byte(privkeyPEM)), } err := d.sdkClient.AddCustomCertificateWithContext(ctx, d.config.PullZoneId, createCertificateReq) d.logger.Debug("sdk request 'bunny.AddCustomCertificate'", slog.Any("request", createCertificateReq)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'bunny.AddCustomCertificate': %w", err) } return &deployer.DeployResult{}, nil } func createSDKClient(apiKey string) (*bunnysdk.Client, error) { return bunnysdk.NewClient(apiKey) } ================================================ FILE: pkg/core/deployer/providers/bunny-cdn/bunny_cdn_test.go ================================================ package bunnycdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/bunny-cdn" ) var ( fInputCertPath string fInputKeyPath string fApiKey string fPullZoneId string fHostName string ) func init() { argsPrefix := "BUNNYCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") flag.StringVar(&fPullZoneId, argsPrefix+"PULLZONEID", "", "") flag.StringVar(&fHostName, argsPrefix+"HOSTNAME", "", "") } /* Shell command to run this test: go test -v ./bunny_cdn_test.go -args \ --BUNNYCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --BUNNYCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --BUNNYCDN_APITOKEN="your-api-token" \ --BUNNYCDN_PULLZONEID="your-pull-zone-id" \ --BUNNYCDN_HOSTNAME="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("APIKEY: %v", fApiKey), fmt.Sprintf("PULLZONEID: %v", fPullZoneId), fmt.Sprintf("HOSTNAME: %v", fHostName), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ApiKey: fApiKey, PullZoneId: fPullZoneId, Hostname: fHostName, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/byteplus-cdn/byteplus_cdn.go ================================================ package bytepluscdn import ( "context" "errors" "fmt" "log/slog" "strings" bpcdn "github.com/byteplus-sdk/byteplus-sdk-golang/service/cdn" bp "github.com/volcengine/volcengine-go-sdk/volcengine" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/byteplus-cdn" "github.com/certimate-go/certimate/pkg/core/deployer" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // BytePlus AccessKey。 AccessKey string `json:"accessKey"` // BytePlus SecretKey。 SecretKey string `json:"secretKey"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *bpcdn.CDN sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client := bpcdn.NewInstance() client.Client.SetAccessKey(config.AccessKey) client.Client.SetSecretKey(config.SecretKey) pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKey: config.AccessKey, SecretKey: config.SecretKey, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名列表 domains := make([]string, 0) switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getMatchedDomainsByWildcard(ctx, d.config.Domain) if err != nil { return nil, err } domains = domainCandidates } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { domainCandidates, err := d.getMatchedDomainsByCertId(ctx, upres.CertId) if err != nil { return nil, err } domains = domainCandidates } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历绑定证书 if len(domains) == 0 { d.logger.Info("no cdn domains to deploy") } else { d.logger.Info("found cdn domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getMatchedDomainsByWildcard(ctx context.Context, wildcardDomain string) ([]string, error) { domains := make([]string, 0) // 查询加速域名列表,获取匹配的域名 // REF: https://docs.byteplus.com/en/docs/byteplus-cdn/ListCdnDomains_en-us listCdnDomainsPageNum := 1 listCdnDomainsPageSize := 100 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } listCdnDomainsReq := &bpcdn.ListCdnDomainsRequest{ Domain: bp.String(strings.TrimPrefix(wildcardDomain, "*.")), Status: bp.String("online"), PageNum: bp.Int64(int64(listCdnDomainsPageNum)), PageSize: bp.Int64(int64(listCdnDomainsPageSize)), } listCdnDomainsResp, err := d.sdkClient.ListCdnDomains(listCdnDomainsReq) d.logger.Debug("sdk request 'cdn.ListCdnDomains'", slog.Any("request", listCdnDomainsReq), slog.Any("response", listCdnDomainsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.ListCdnDomains': %w", err) } for _, domainItem := range listCdnDomainsResp.Result.Data { if xcerthostname.IsMatch(wildcardDomain, domainItem.Domain) { domains = append(domains, domainItem.Domain) } } if len(listCdnDomainsResp.Result.Data) < listCdnDomainsPageSize { break } listCdnDomainsPageSize++ } return domains, nil } func (d *Deployer) getMatchedDomainsByCertId(ctx context.Context, cloudCertId string) ([]string, error) { domains := make([]string, 0) // 获取指定证书可关联的域名 // REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-describecertconfig-9ea17 describeCertConfigReq := &bpcdn.DescribeCertConfigRequest{ CertId: cloudCertId, } describeCertConfigResp, err := d.sdkClient.DescribeCertConfig(describeCertConfigReq) d.logger.Debug("sdk request 'cdn.DescribeCertConfig'", slog.Any("request", describeCertConfigReq), slog.Any("response", describeCertConfigResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.DescribeCertConfig': %w", err) } if describeCertConfigResp.Result.CertNotConfig != nil { for i := range describeCertConfigResp.Result.CertNotConfig { domains = append(domains, describeCertConfigResp.Result.CertNotConfig[i].Domain) } } if describeCertConfigResp.Result.OtherCertConfig != nil { for i := range describeCertConfigResp.Result.OtherCertConfig { domains = append(domains, describeCertConfigResp.Result.OtherCertConfig[i].Domain) } } if len(domains) == 0 { if len(describeCertConfigResp.Result.SpecifiedCertConfig) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error { // 关联证书与加速域名 // REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-batchdeploycert batchDeployCertReq := &bpcdn.BatchDeployCertRequest{ CertId: cloudCertId, Domain: domain, } batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq) d.logger.Debug("sdk request 'cdn.BatchDeployCert'", slog.Any("request", batchDeployCertReq), slog.Any("response", batchDeployCertResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'cdn.BatchDeployCert': %w", err) } return nil } ================================================ FILE: pkg/core/deployer/providers/byteplus-cdn/byteplus_cdn_test.go ================================================ package bytepluscdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/byteplus-cdn" ) var ( fInputCertPath string fInputKeyPath string fAccessKey string fSecretKey string fDomain string ) func init() { argsPrefix := "BYTEPLUSCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "") flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./byteplus_cdn_test.go -args \ --BYTEPLUSCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --BYTEPLUSCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --BYTEPLUSCDN_ACCESSKEY="your-access-key" \ --BYTEPLUSCDN_SECRETKEY="your-secret-key" \ --BYTEPLUSCDN_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEY: %v", fAccessKey), fmt.Sprintf("SECRETKEY: %v", fSecretKey), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKey: fAccessKey, SecretKey: fSecretKey, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/byteplus-cdn/consts.go ================================================ package bytepluscdn const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/cachefly/cachefly.go ================================================ package cachefly import ( "context" "errors" "fmt" "log/slog" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/deployer" cacheflysdk "github.com/certimate-go/certimate/pkg/sdk3rd/cachefly" ) type DeployerConfig struct { // CacheFly API Token。 ApiToken string `json:"apiToken"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *cacheflysdk.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("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) (*deployer.DeployResult, error) { // 上传证书 // REF: https://api.cachefly.com/api/2.5/docs#tag/Certificates/paths/~1certificates/post createCertificateReq := &cacheflysdk.CreateCertificateRequest{ Certificate: lo.ToPtr(certPEM), CertificateKey: lo.ToPtr(privkeyPEM), } createCertificateResp, err := d.sdkClient.CreateCertificateWithContext(ctx, createCertificateReq) d.logger.Debug("sdk request 'cachefly.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cachefly.CreateCertificate': %w", err) } return &deployer.DeployResult{}, nil } func createSDKClient(apiToken string) (*cacheflysdk.Client, error) { return cacheflysdk.NewClient(apiToken) } ================================================ FILE: pkg/core/deployer/providers/cachefly/cachefly_test.go ================================================ package cachefly_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/cachefly" ) var ( fInputCertPath string fInputKeyPath string fApiToken string ) func init() { argsPrefix := "CACHEFLY_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "") } /* Shell command to run this test: go test -v ./cachefly_test.go -args \ --CACHEFLY_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --CACHEFLY_INPUTKEYPATH="/path/to/your-input-key.pem" \ --CACHEFLY_APITOKEN="your-api-token" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("APITOKEN: %v", fApiToken), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ApiToken: fApiToken, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/cdnfly/cdnfly.go ================================================ package cdnfly import ( "context" "crypto/tls" "encoding/json" "errors" "fmt" "log/slog" "time" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/deployer" cdnflysdk "github.com/certimate-go/certimate/pkg/sdk3rd/cdnfly" ) type DeployerConfig struct { // Cdnfly 服务地址。 ServerUrl string `json:"serverUrl"` // Cdnfly 用户端 API Key。 ApiKey string `json:"apiKey"` // Cdnfly 用户端 API Secret。 ApiSecret string `json:"apiSecret"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 网站 ID。 // 部署资源类型为 [RESOURCE_TYPE_WEBSITE] 时必填。 SiteId string `json:"siteId,omitempty"` // 证书 ID。 // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。 CertificateId string `json:"certificateId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *cdnflysdk.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ServerUrl, config.ApiKey, config.ApiSecret, config.AllowInsecureConnections) 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) (*deployer.DeployResult, error) { // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_WEBSITE: if err := d.deployToSite(ctx, certPEM, privkeyPEM); err != nil { return nil, err } case RESOURCE_TYPE_CERTIFICATE: if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToSite(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.SiteId == "" { return errors.New("config `siteId` is required") } // 获取单个网站详情 // REF: https://doc.cdnfly.cn/wangzhanguanli-v1-sites.html#%E8%8E%B7%E5%8F%96%E5%8D%95%E4%B8%AA%E7%BD%91%E7%AB%99%E8%AF%A6%E6%83%85 getSiteResp, err := d.sdkClient.GetSiteWithContext(ctx, d.config.SiteId) d.logger.Debug("sdk request 'cdnfly.GetSite'", slog.String("siteId", d.config.SiteId), slog.Any("response", getSiteResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'cdnfly.GetSite': %w", err) } // 添加单个证书 // REF: https://doc.cdnfly.cn/wangzhanzhengshu-v1-certs.html#%E6%B7%BB%E5%8A%A0%E5%8D%95%E4%B8%AA%E6%88%96%E5%A4%9A%E4%B8%AA%E8%AF%81%E4%B9%A6-%E5%A4%9A%E4%B8%AA%E8%AF%81%E4%B9%A6%E6%97%B6%E6%95%B0%E6%8D%AE%E6%A0%BC%E5%BC%8F%E4%B8%BA%E6%95%B0%E7%BB%84 createCertificateReq := &cdnflysdk.CreateCertRequest{ Name: lo.ToPtr(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())), Type: lo.ToPtr("custom"), Cert: lo.ToPtr(certPEM), Key: lo.ToPtr(privkeyPEM), } createCertificateResp, err := d.sdkClient.CreateCertWithContext(ctx, createCertificateReq) d.logger.Debug("sdk request 'cdnfly.CreateCert'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'cdnfly.CreateCert': %w", err) } // 修改单个网站 // REF: https://doc.cdnfly.cn/wangzhanguanli-v1-sites.html#%E4%BF%AE%E6%94%B9%E5%8D%95%E4%B8%AA%E7%BD%91%E7%AB%99 updateSiteHttpsListenMap := make(map[string]any) _ = json.Unmarshal([]byte(getSiteResp.Data.HttpsListen), &updateSiteHttpsListenMap) updateSiteHttpsListenMap["cert"] = createCertificateResp.Data updateSiteHttpsListenData, _ := json.Marshal(updateSiteHttpsListenMap) updateSiteReq := &cdnflysdk.UpdateSiteRequest{ HttpsListen: lo.ToPtr(string(updateSiteHttpsListenData)), } updateSiteResp, err := d.sdkClient.UpdateSiteWithContext(ctx, d.config.SiteId, updateSiteReq) d.logger.Debug("sdk request 'cdnfly.UpdateSite'", slog.String("siteId", d.config.SiteId), slog.Any("request", updateSiteReq), slog.Any("response", updateSiteResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'cdnfly.UpdateSite': %w", err) } return nil } func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.CertificateId == "" { return errors.New("config `certificateId` is required") } // 修改单个证书 // REF: https://doc.cdnfly.cn/wangzhanzhengshu-v1-certs.html#%E4%BF%AE%E6%94%B9%E5%8D%95%E4%B8%AA%E8%AF%81%E4%B9%A6 updateCertReq := &cdnflysdk.UpdateCertRequest{ Type: lo.ToPtr("custom"), Cert: lo.ToPtr(certPEM), Key: lo.ToPtr(privkeyPEM), } updateCertResp, err := d.sdkClient.UpdateCertWithContext(ctx, d.config.CertificateId, updateCertReq) d.logger.Debug("sdk request 'cdnfly.UpdateCert'", slog.String("certId", d.config.CertificateId), slog.Any("request", updateCertReq), slog.Any("response", updateCertResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'cdnfly.UpdateCert': %w", err) } return nil } func createSDKClient(serverUrl, apiKey, apiSecret string, skipTlsVerify bool) (*cdnflysdk.Client, error) { client, err := cdnflysdk.NewClient(serverUrl, apiKey, apiSecret) if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } ================================================ FILE: pkg/core/deployer/providers/cdnfly/cdnfly_test.go ================================================ package cdnfly_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/cdnfly" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fApiKey string fApiSecret string fCertificateId string ) func init() { argsPrefix := "CDNFLY_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") flag.StringVar(&fApiSecret, argsPrefix+"APISECRET", "", "") flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "") } /* Shell command to run this test: go test -v ./cdnfly_test.go -args \ --CDNFLY_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --CDNFLY_INPUTKEYPATH="/path/to/your-input-key.pem" \ --CDNFLY_SERVERURL="http://127.0.0.1:88" \ --CDNFLY_APIKEY="your-api-key" \ --CDNFLY_APISECRET="your-api-secret" \ --CDNFLY_CERTIFICATEID="your-cert-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("APIKEY: %v", fApiKey), fmt.Sprintf("APISECRET: %v", fApiSecret), fmt.Sprintf("CERTIFICATEID: %v", fCertificateId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ServerUrl: fServerUrl, ApiKey: fApiKey, ApiSecret: fApiSecret, AllowInsecureConnections: true, ResourceType: provider.RESOURCE_TYPE_CERTIFICATE, CertificateId: fCertificateId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/cdnfly/consts.go ================================================ package cdnfly const ( // 资源类型:替换指定网站的证书。 RESOURCE_TYPE_WEBSITE = "website" // 资源类型:替换指定证书。 RESOURCE_TYPE_CERTIFICATE = "certificate" ) ================================================ FILE: pkg/core/deployer/providers/cpanel/consts.go ================================================ package cpanel const ( // 资源类型:替换指定网站的证书。 RESOURCE_TYPE_WEBSITE = "website" ) ================================================ FILE: pkg/core/deployer/providers/cpanel/cpanel.go ================================================ package cpanel import ( "context" "crypto/tls" "errors" "fmt" "log/slog" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/deployer" cpanelsdk "github.com/certimate-go/certimate/pkg/sdk3rd/cpanel" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type DeployerConfig struct { // cPanel 服务地址。 ServerUrl string `json:"serverUrl"` // cPanel 用户名。 Username string `json:"username"` // cPanel 接口密钥。 ApiToken string `json:"apiToken"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 网站域名(不支持泛域名)。 // 部署资源类型为 [RESOURCE_TYPE_WEBSITE] 时必填。 Domain string `json:"domain,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *cpanelsdk.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ServerUrl, config.Username, config.ApiToken, config.AllowInsecureConnections) 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) (*deployer.DeployResult, error) { // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_WEBSITE: if err := d.deployToWebsite(ctx, certPEM, privkeyPEM); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToWebsite(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.Domain == "" { return errors.New("config `domain` is required") } // 提取服务器证书和中间证书 serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM) if err != nil { return fmt.Errorf("failed to extract certs: %w", err) } // 安装 SSL 证书 // REF: https://api.docs.cpanel.net/openapi/cpanel/operation/install_ssl/ sslInstallSSLReq := &cpanelsdk.SSLInstallSSLRequest{ Domain: lo.ToPtr(d.config.Domain), Cert: lo.ToPtr(serverCertPEM), Key: lo.ToPtr(privkeyPEM), CABundle: lo.ToPtr(intermediaCertPEM), } sslInstallSSLResp, err := d.sdkClient.SSLInstallSSL(sslInstallSSLReq) d.logger.Debug("sdk request 'SSL.install_ssl'", slog.Any("request", sslInstallSSLReq), slog.Any("response", sslInstallSSLResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'SSL.install_ssl': %w", err) } return nil } func createSDKClient(serverUrl, username, apiToken string, skipTlsVerify bool) (*cpanelsdk.Client, error) { client, err := cpanelsdk.NewClient(serverUrl, username, apiToken) if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } ================================================ FILE: pkg/core/deployer/providers/cpanel/cpanel_test.go ================================================ package cpanel_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/cpanel" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fUsername string fApiToken string fDomain string ) func init() { argsPrefix := "CPANEL_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "") flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./cpanel_test.go -args \ --CPANEL_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --CPANEL_INPUTKEYPATH="/path/to/your-input-key.pem" \ --CPANEL_SERVERURL="http://127.0.0.1:2082" \ --CPANEL_USERNAME="your-username" \ --CPANEL_APITOKEN="your-api-token" \ --CPANEL_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("USERNAME: %v", fUsername), fmt.Sprintf("APITOKEN: %v", fApiToken), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ServerUrl: fServerUrl, Username: fUsername, ApiToken: fApiToken, AllowInsecureConnections: true, ResourceType: provider.RESOURCE_TYPE_WEBSITE, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/ctcccloud-ao/consts.go ================================================ package ctcccloudao const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/ctcccloud-ao/ctcccloud_ao.go ================================================ package ctcccloudao import ( "context" "errors" "fmt" "log/slog" "strconv" "strings" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-ao" "github.com/certimate-go/certimate/pkg/core/deployer" ctyunao "github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/ao" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 天翼云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 天翼云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *ctyunao.Client sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, SecretAccessKey: config.SecretAccessKey, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, 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) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return xcerthostname.IsMatch(d.config.Domain, domain) }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by wildcard") } } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no accessone domains to deploy") } else { d.logger.Info("found accessone domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, upres.CertName); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 查询域名列表 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13816&data=174&isNormal=1&vid=167 queryDomainsPage := 1 queryDomainsPageSize := 100 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } queryDomainsReq := &ctyunao.QueryDomainsRequest{ Page: lo.ToPtr(int32(queryDomainsPage)), PageSize: lo.ToPtr(int32(queryDomainsPageSize)), ProductCode: lo.ToPtr("020"), } queryDomainsResp, err := d.sdkClient.QueryDomainsWithContext(ctx, queryDomainsReq) d.logger.Debug("sdk request 'cdn.QueryDomains'", slog.Any("request", queryDomainsReq), slog.Any("response", queryDomainsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.QueryDomains': %w", err) } if queryDomainsResp.ReturnObj == nil { break } ignoredStatuses := []int32{1, 5, 6, 7, 8, 9, 11, 12} for _, domainItem := range queryDomainsResp.ReturnObj.Results { if lo.Contains(ignoredStatuses, domainItem.Status) { continue } domains = append(domains, domainItem.Domain) } if len(queryDomainsResp.ReturnObj.Results) < queryDomainsPageSize { break } queryDomainsPage++ } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertName string) error { // 域名基础及加速配置查询 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13412&data=174&isNormal=1&vid=167 getDomainConfigReq := &ctyunao.GetDomainConfigRequest{ Domain: lo.ToPtr(domain), ProductCode: lo.ToPtr("020"), } getDomainConfigResp, err := d.sdkClient.GetDomainConfigWithContext(ctx, getDomainConfigReq) d.logger.Debug("sdk request 'cdn.GetDomainConfig'", slog.Any("request", getDomainConfigReq), slog.Any("response", getDomainConfigResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'cdn.GetDomainConfig': %w", err) } // 域名基础及加速配置修改 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13413&data=174&isNormal=1&vid=167 modifyDomainConfigReq := &ctyunao.ModifyDomainConfigRequest{ Domain: lo.ToPtr(domain), ProductCode: lo.ToPtr(getDomainConfigResp.ReturnObj.ProductCode), Origin: lo.Map(getDomainConfigResp.ReturnObj.Origin, func(item *ctyunao.DomainOriginConfigWithWeight, _ int) *ctyunao.DomainOriginConfig { weight := item.Weight if weight == 0 { weight = 1 } return &ctyunao.DomainOriginConfig{ Origin: item.Origin, Role: item.Role, Weight: strconv.Itoa(int(weight)), } }), HttpsStatus: lo.ToPtr("on"), CertName: lo.ToPtr(cloudCertName), } modifyDomainConfigResp, err := d.sdkClient.ModifyDomainConfigWithContext(ctx, modifyDomainConfigReq) d.logger.Debug("sdk request 'cdn.ModifyDomainConfig'", slog.Any("request", modifyDomainConfigReq), slog.Any("response", modifyDomainConfigResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'cdn.ModifyDomainConfig': %w", err) } return nil } func createSDKClient(accessKeyId, secretAccessKey string) (*ctyunao.Client, error) { return ctyunao.NewClient(accessKeyId, secretAccessKey) } ================================================ FILE: pkg/core/deployer/providers/ctcccloud-ao/ctcccloud_ao_test.go ================================================ package ctcccloudao_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-ao" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string fDomain string ) func init() { argsPrefix := "CTCCCLOUDAO_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./ctcccloud_ao_test.go -args \ --CTCCCLOUDAO_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --CTCCCLOUDAO_INPUTKEYPATH="/path/to/your-input-key.pem" \ --CTCCCLOUDAO_ACCESSKEYID="your-access-key-id" \ --CTCCCLOUDAO_SECRETACCESSKEY="your-secret-access-key" \ --CTCCCLOUDAO_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/ctcccloud-cdn/consts.go ================================================ package ctcccloudcdn const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/ctcccloud-cdn/ctcccloud_cdn.go ================================================ package ctcccloudcdn import ( "context" "errors" "fmt" "log/slog" "strings" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-cdn" "github.com/certimate-go/certimate/pkg/core/deployer" ctyuncdn "github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/cdn" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 天翼云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 天翼云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *ctyuncdn.Client sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, SecretAccessKey: config.SecretAccessKey, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, 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) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return xcerthostname.IsMatch(d.config.Domain, domain) }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by wildcard") } } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no cdn domains to deploy") } else { d.logger.Info("found cdn domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, upres.CertName); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 查询域名列表 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=11307&data=161&isNormal=1&vid=154 queryDomainListPage := 1 queryDomainListPageSize := 100 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } queryDomainListReq := &ctyuncdn.QueryDomainListRequest{ Page: lo.ToPtr(int32(queryDomainListPage)), PageSize: lo.ToPtr(int32(queryDomainListPageSize)), ProductCode: lo.ToPtr("020"), } queryDomainListResp, err := d.sdkClient.QueryDomainListWithContext(ctx, queryDomainListReq) d.logger.Debug("sdk request 'cdn.QueryDomainList'", slog.Any("request", queryDomainListReq), slog.Any("response", queryDomainListResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.QueryDomainList': %w", err) } if queryDomainListResp.ReturnObj == nil { break } filteredProductCodes := []string{"001", "003", "004", "008"} ignoredStatuses := []int32{1, 5, 6, 7, 8, 9, 11, 12} for _, domainItem := range queryDomainListResp.ReturnObj.Results { if !lo.Contains(filteredProductCodes, domainItem.ProductCode) { continue } if lo.Contains(ignoredStatuses, domainItem.Status) { continue } domains = append(domains, domainItem.Domain) } if len(queryDomainListResp.ReturnObj.Results) < queryDomainListPageSize { break } queryDomainListPage++ } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertName string) error { // 查询域名配置信息 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=11304&data=161&isNormal=1&vid=154 queryDomainDetailReq := &ctyuncdn.QueryDomainDetailRequest{ Domain: lo.ToPtr(domain), } queryDomainDetailResp, err := d.sdkClient.QueryDomainDetailWithContext(ctx, queryDomainDetailReq) d.logger.Debug("sdk request 'cdn.QueryDomainDetail'", slog.Any("request", queryDomainDetailReq), slog.Any("response", queryDomainDetailResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'cdn.QueryDomainDetail': %w", err) } // 修改域名配置 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=11308&data=161&isNormal=1&vid=154 updateDomainReq := &ctyuncdn.UpdateDomainRequest{ Domain: lo.ToPtr(domain), HttpsStatus: lo.ToPtr("on"), CertName: lo.ToPtr(cloudCertName), } updateDomainResp, err := d.sdkClient.UpdateDomainWithContext(ctx, updateDomainReq) d.logger.Debug("sdk request 'cdn.UpdateDomain'", slog.Any("request", updateDomainReq), slog.Any("response", updateDomainResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'cdn.UpdateDomain': %w", err) } return nil } func createSDKClient(accessKeyId, secretAccessKey string) (*ctyuncdn.Client, error) { return ctyuncdn.NewClient(accessKeyId, secretAccessKey) } ================================================ FILE: pkg/core/deployer/providers/ctcccloud-cdn/ctcccloud_cdn_test.go ================================================ package ctcccloudcdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-cdn" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string fDomain string ) func init() { argsPrefix := "CTCCCLOUDCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./ctcccloud_cdn_test.go -args \ --CTCCCLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --CTCCCLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --CTCCCLOUDCDN_ACCESSKEYID="your-access-key-id" \ --CTCCCLOUDCDN_SECRETACCESSKEY="your-secret-access-key" \ --CTCCCLOUDCDN_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/ctcccloud-cms/ctcccloud_cms.go ================================================ package ctcccloudcms import ( "context" "errors" "fmt" "log/slog" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-cms" "github.com/certimate-go/certimate/pkg/core/deployer" ) type DeployerConfig struct { // 天翼云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 天翼云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, SecretAccessKey: config.SecretAccessKey, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkCertmgr: pcertmgr, }, 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) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } return &deployer.DeployResult{}, nil } ================================================ FILE: pkg/core/deployer/providers/ctcccloud-cms/ctcccloud_cms_test.go ================================================ package ctcccloudcms_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-cms" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string ) func init() { argsPrefix := "CTCCCLOUDCMS_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") } /* Shell command to run this test: go test -v ./ctcccloud_cms_test.go -args \ --CTCCCLOUDCMS_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --CTCCCLOUDCMS_INPUTKEYPATH="/path/to/your-input-key.pem" \ --CTCCCLOUDCMS_ACCESSKEYID="your-access-key-id" \ --CTCCCLOUDCMS_SECRETACCESSKEY="your-secret-access-key" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/ctcccloud-elb/consts.go ================================================ package ctcccloudelb const ( // 资源类型:部署到指定负载均衡器。 RESOURCE_TYPE_LOADBALANCER = "loadbalancer" // 资源类型:部署到指定监听器。 RESOURCE_TYPE_LISTENER = "listener" ) ================================================ FILE: pkg/core/deployer/providers/ctcccloud-elb/ctcccloud_elb.go ================================================ package ctcccloudelb import ( "context" "errors" "fmt" "log/slog" "strings" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-elb" "github.com/certimate-go/certimate/pkg/core/deployer" ctyunelb "github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/elb" ) type DeployerConfig struct { // 天翼云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 天翼云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // 天翼云资源池 ID。 RegionId string `json:"regionId"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 负载均衡实例 ID。 // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER] 时必填。 LoadbalancerId string `json:"loadbalancerId,omitempty"` // 负载均衡监听器 ID。 // 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。 ListenerId string `json:"listenerId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *ctyunelb.Client sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, SecretAccessKey: config.SecretAccessKey, RegionId: config.RegionId, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, 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) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_LOADBALANCER: if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil { return nil, err } case RESOURCE_TYPE_LISTENER: if err := d.deployToListener(ctx, upres.CertId); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error { if d.config.LoadbalancerId == "" { return errors.New("config `loadbalancerId` is required") } // 查询监听列表 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=24&api=5654&data=88&isNormal=1&vid=82 listenerIds := make([]string, 0) { listListenersReq := &ctyunelb.ListListenersRequest{ RegionID: lo.ToPtr(d.config.RegionId), LoadBalancerID: lo.ToPtr(d.config.LoadbalancerId), } listListenersResp, err := d.sdkClient.ListListenersWithContext(ctx, listListenersReq) d.logger.Debug("sdk request 'elb.ListListeners'", slog.Any("request", listListenersReq), slog.Any("response", listListenersResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'elb.ListListeners': %w", err) } for _, listener := range listListenersResp.ReturnObj { if strings.EqualFold(listener.Protocol, "HTTPS") { listenerIds = append(listenerIds, listener.ID) } } } // 遍历更新监听证书 if len(listenerIds) == 0 { d.logger.Info("no elb listeners to deploy") } else { d.logger.Info("found https listeners to deploy", slog.Any("listenerIds", listenerIds)) var errs []error for _, listenerId := range listenerIds { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error { if d.config.ListenerId == "" { return errors.New("config `listenerId` is required") } // 更新监听 if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil { return err } return nil } func (d *Deployer) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error { // 更新监听器 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=24&api=5652&data=88&isNormal=1&vid=82 setLoadBalancerHTTPSListenerAttributeReq := &ctyunelb.UpdateListenerRequest{ RegionID: lo.ToPtr(d.config.RegionId), ListenerID: lo.ToPtr(cloudListenerId), CertificateID: lo.ToPtr(cloudCertId), } setLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.UpdateListenerWithContext(ctx, setLoadBalancerHTTPSListenerAttributeReq) d.logger.Debug("sdk request 'elb.UpdateListener'", slog.Any("request", setLoadBalancerHTTPSListenerAttributeReq), slog.Any("response", setLoadBalancerHTTPSListenerAttributeResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'elb.UpdateListener': %w", err) } return nil } func createSDKClient(accessKeyId, secretAccessKey string) (*ctyunelb.Client, error) { return ctyunelb.NewClient(accessKeyId, secretAccessKey) } ================================================ FILE: pkg/core/deployer/providers/ctcccloud-elb/ctcccloud_elb_test.go ================================================ package ctcccloudelb_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-elb" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string fRegionId string fLoadbalancerId string fListenerId string ) func init() { argsPrefix := "CTCCCLOUDELB_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") flag.StringVar(&fRegionId, argsPrefix+"REGIONID", "", "") flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "") flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "") } /* Shell command to run this test: go test -v ./ctcccloud_elb_test.go -args \ --CTCCCLOUDELB_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --CTCCCLOUDELB_INPUTKEYPATH="/path/to/your-input-key.pem" \ --CTCCCLOUDELB_ACCESSKEYID="your-access-key-id" \ --CTCCCLOUDELB_SECRETACCESSKEY="your-secret-access-key" \ --CTCCCLOUDELB_REGIONID="your-region-id" \ --CTCCCLOUDELB_LOADBALANCERID="your-elb-instance-id" \ --CTCCCLOUDELB_LISTENERID="your-elb-listener-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy_ToLoadbalancer", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), fmt.Sprintf("REGIONID: %v", fRegionId), fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, RegionId: fRegionId, ResourceType: provider.RESOURCE_TYPE_LOADBALANCER, LoadbalancerId: fLoadbalancerId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) t.Run("Deploy_ToListener", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), fmt.Sprintf("REGIONID: %v", fRegionId), fmt.Sprintf("LISTENERID: %v", fListenerId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, RegionId: fRegionId, ResourceType: provider.RESOURCE_TYPE_LISTENER, ListenerId: fListenerId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/ctcccloud-faas/ctcccloud_faas.go ================================================ package ctcccloudfaas import ( "context" "errors" "fmt" "log/slog" "strings" "time" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" "github.com/certimate-go/certimate/pkg/core/deployer" ctyunfaas "github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/faas" ) type DeployerConfig struct { // 天翼云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 天翼云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // 天翼云资源池 ID。 RegionId string `json:"regionId"` // 自定义域名(不支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *ctyunfaas.Client sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey) 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) (*deployer.DeployResult, error) { if d.config.RegionId == "" { return nil, errors.New("config `regionId` is required") } if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } // 获取自定义域名配置 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=53&api=16002&data=42&isNormal=1&vid=40 var faasCustomDomain *ctyunfaas.CustomDomainRecord getCustomDomainReq := &ctyunfaas.GetCustomDomainRequest{ RegionId: lo.ToPtr(d.config.RegionId), DomainName: lo.ToPtr(d.config.Domain), CnameCheck: lo.ToPtr(false), } getCustomDomainResp, err := d.sdkClient.GetCustomDomainWithContext(ctx, getCustomDomainReq) d.logger.Debug("sdk request 'faas.GetCustomDomain'", slog.Any("request", getCustomDomainReq), slog.Any("response", getCustomDomainResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'faas.GetCustomDomain': %w", err) } else { faasCustomDomain = getCustomDomainResp.ReturnObj // 已部署过此域名,跳过 if faasCustomDomain.CertConfig != nil && faasCustomDomain.CertConfig.Certificate == certPEM && faasCustomDomain.CertConfig.PrivateKey == privkeyPEM { return &deployer.DeployResult{}, nil } } // 更新自定义域名 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=53&api=16004&data=42&isNormal=1&vid=40 updateCustomDomainReq := &ctyunfaas.UpdateCustomDomainRequest{ RegionId: lo.ToPtr(d.config.RegionId), DomainName: lo.ToPtr(d.config.Domain), Protocol: lo.ToPtr(faasCustomDomain.Protocol), AuthConfig: faasCustomDomain.AuthConfig, CertConfig: &ctyunfaas.CustomDomainCertConfig{ CertName: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()), Certificate: certPEM, PrivateKey: privkeyPEM, }, } if !strings.Contains(*updateCustomDomainReq.Protocol, "HTTPS") { if *updateCustomDomainReq.Protocol == "" { updateCustomDomainReq.Protocol = lo.ToPtr("HTTPS") } else { updateCustomDomainReq.Protocol = lo.ToPtr(*updateCustomDomainReq.Protocol + ",HTTPS") } } updateCustomDomainResp, err := d.sdkClient.UpdateCustomDomainWithContext(ctx, updateCustomDomainReq) d.logger.Debug("sdk request 'faas.UpdateCustomDomain'", slog.Any("request", updateCustomDomainReq), slog.Any("response", updateCustomDomainResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'faas.UpdateCustomDomain': %w", err) } return &deployer.DeployResult{}, nil } func createSDKClient(accessKeyId, secretAccessKey string) (*ctyunfaas.Client, error) { return ctyunfaas.NewClient(accessKeyId, secretAccessKey) } ================================================ FILE: pkg/core/deployer/providers/ctcccloud-faas/ctcccloud_faas_test.go ================================================ package ctcccloudfaas_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-faas" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string fRegionId string fDomain string ) func init() { argsPrefix := "CTCCCLOUDFAAS_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") flag.StringVar(&fRegionId, argsPrefix+"REGIONID", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./ctcccloud_faas_test.go -args \ --CTCCCLOUDFAAS_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --CTCCCLOUDFAAS_INPUTKEYPATH="/path/to/your-input-key.pem" \ --CTCCCLOUDFAAS_ACCESSKEYID="your-access-key-id" \ --CTCCCLOUDFAAS_SECRETACCESSKEY="your-secret-access-key" \ --CTCCCLOUDFAAS_REGIONID="your-region-id" \ --CTCCCLOUDFAAS_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), fmt.Sprintf("REGIONID: %v", fRegionId), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, RegionId: fRegionId, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/ctcccloud-icdn/consts.go ================================================ package ctcccloudicdn const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/ctcccloud-icdn/ctcccloud_icdn.go ================================================ package ctcccloudicdn import ( "context" "errors" "fmt" "log/slog" "strings" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-icdn" "github.com/certimate-go/certimate/pkg/core/deployer" ctyunicdn "github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/icdn" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 天翼云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 天翼云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *ctyunicdn.Client sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, SecretAccessKey: config.SecretAccessKey, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, 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) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return xcerthostname.IsMatch(d.config.Domain, domain) }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by wildcard") } } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no icdn domains to deploy") } else { d.logger.Info("found icdn domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, upres.CertName); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 查询域名列表 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10852&data=173&isNormal=1&vid=166 queryDomainsPage := 1 queryDomainsPageSize := 100 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } queryDomainListReq := &ctyunicdn.QueryDomainListRequest{ Page: lo.ToPtr(int32(queryDomainsPage)), PageSize: lo.ToPtr(int32(queryDomainsPageSize)), ProductCode: lo.ToPtr("006"), } queryDomainListResp, err := d.sdkClient.QueryDomainListWithContext(ctx, queryDomainListReq) d.logger.Debug("sdk request 'cdn.QueryDomainList'", slog.Any("request", queryDomainListReq), slog.Any("response", queryDomainListResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.QueryDomainList': %w", err) } if queryDomainListResp.ReturnObj == nil { break } ignoredStatuses := []int32{1, 5, 6, 7, 8, 9, 11, 12} for _, domainItem := range queryDomainListResp.ReturnObj.Results { if lo.Contains(ignoredStatuses, domainItem.Status) { continue } domains = append(domains, domainItem.Domain) } if len(queryDomainListResp.ReturnObj.Results) < queryDomainsPageSize { break } queryDomainsPage++ } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertName string) error { // 查询域名配置信息 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10849&data=173&isNormal=1&vid=166 queryDomainDetailReq := &ctyunicdn.QueryDomainDetailRequest{ Domain: lo.ToPtr(domain), } queryDomainDetailResp, err := d.sdkClient.QueryDomainDetailWithContext(ctx, queryDomainDetailReq) d.logger.Debug("sdk request 'icdn.QueryDomainDetail'", slog.Any("request", queryDomainDetailReq), slog.Any("response", queryDomainDetailResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'icdn.QueryDomainDetail': %w", err) } // 修改域名配置 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10853&data=173&isNormal=1&vid=166 updateDomainReq := &ctyunicdn.UpdateDomainRequest{ Domain: lo.ToPtr(domain), HttpsStatus: lo.ToPtr("on"), CertName: lo.ToPtr(cloudCertName), } updateDomainResp, err := d.sdkClient.UpdateDomainWithContext(ctx, updateDomainReq) d.logger.Debug("sdk request 'icdn.UpdateDomain'", slog.Any("request", updateDomainReq), slog.Any("response", updateDomainResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'icdn.UpdateDomain': %w", err) } return nil } func createSDKClient(accessKeyId, secretAccessKey string) (*ctyunicdn.Client, error) { return ctyunicdn.NewClient(accessKeyId, secretAccessKey) } ================================================ FILE: pkg/core/deployer/providers/ctcccloud-icdn/ctcccloud_icdn_test.go ================================================ package ctcccloudicdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-icdn" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string fDomain string ) func init() { argsPrefix := "CTCCCLOUDCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./ctcccloud_cdn_test.go -args \ --CTCCCLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --CTCCCLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --CTCCCLOUDCDN_ACCESSKEYID="your-access-key-id" \ --CTCCCLOUDCDN_SECRETACCESSKEY="your-secret-access-key" \ --CTCCCLOUDCDN_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/ctcccloud-lvdn/consts.go ================================================ package ctcccloudlvdn const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/ctcccloud-lvdn/ctcccloud_lvdn.go ================================================ package ctcccloudlvdn import ( "context" "errors" "fmt" "log/slog" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ctcccloud-lvdn" "github.com/certimate-go/certimate/pkg/core/deployer" ctyunlvdn "github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/lvdn" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type DeployerConfig struct { // 天翼云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 天翼云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名(不支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *ctyunlvdn.Client sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, SecretAccessKey: config.SecretAccessKey, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, 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) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no lvdn domains to deploy") } else { d.logger.Info("found lvdn domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, upres.CertName); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 查询域名列表 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11559&data=183&isNormal=1&vid=261 queryDomainsPage := 1 queryDomainsPageSize := 100 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } queryDomainListReq := &ctyunlvdn.QueryDomainListRequest{ Page: lo.ToPtr(int32(queryDomainsPage)), PageSize: lo.ToPtr(int32(queryDomainsPageSize)), ProductCode: lo.ToPtr("005"), } queryDomainListResp, err := d.sdkClient.QueryDomainListWithContext(ctx, queryDomainListReq) d.logger.Debug("sdk request 'cdn.QueryDomainList'", slog.Any("request", queryDomainListReq), slog.Any("response", queryDomainListResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.QueryDomainList': %w", err) } if queryDomainListResp.ReturnObj == nil { break } ignoredStatuses := []int32{1, 5, 6, 7, 8, 9, 11, 12} for _, domainItem := range queryDomainListResp.ReturnObj.Results { if lo.Contains(ignoredStatuses, domainItem.Status) { continue } domains = append(domains, domainItem.Domain) } if len(queryDomainListResp.ReturnObj.Results) < queryDomainsPageSize { break } queryDomainsPage++ } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertName string) error { // 查询域名配置信息 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11473&data=183&isNormal=1&vid=261 queryDomainDetailReq := &ctyunlvdn.QueryDomainDetailRequest{ Domain: lo.ToPtr(domain), ProductCode: lo.ToPtr("005"), } queryDomainDetailResp, err := d.sdkClient.QueryDomainDetailWithContext(ctx, queryDomainDetailReq) d.logger.Debug("sdk request 'lvdn.QueryDomainDetail'", slog.Any("request", queryDomainDetailReq), slog.Any("response", queryDomainDetailResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'lvdn.QueryDomainDetail': %w", err) } // 修改域名配置 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=11308&data=161&isNormal=1&vid=154 updateDomainReq := &ctyunlvdn.UpdateDomainRequest{ Domain: lo.ToPtr(domain), ProductCode: lo.ToPtr("005"), HttpsSwitch: lo.ToPtr(int32(1)), CertName: lo.ToPtr(cloudCertName), } updateDomainResp, err := d.sdkClient.UpdateDomainWithContext(ctx, updateDomainReq) d.logger.Debug("sdk request 'lvdn.UpdateDomain'", slog.Any("request", updateDomainReq), slog.Any("response", updateDomainResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'lvdn.UpdateDomain': %w", err) } return nil } func createSDKClient(accessKeyId, secretAccessKey string) (*ctyunlvdn.Client, error) { return ctyunlvdn.NewClient(accessKeyId, secretAccessKey) } ================================================ FILE: pkg/core/deployer/providers/ctcccloud-lvdn/ctcccloud_lvdn_test.go ================================================ package ctcccloudlvdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/ctcccloud-lvdn" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string fDomain string ) func init() { argsPrefix := "CTCCCLOUDLVDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./ctcccloud_lvdn_test.go -args \ --CTCCCLOUDLVDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --CTCCCLOUDLVDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --CTCCCLOUDLVDN_ACCESSKEYID="your-access-key-id" \ --CTCCCLOUDLVDN_SECRETACCESSKEY="your-secret-access-key" \ --CTCCCLOUDLVDN_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/dogecloud-cdn/consts.go ================================================ package dogecloudcdn const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/dogecloud-cdn/dogecloud_cdn.go ================================================ package dogecloudcdn import ( "context" "errors" "fmt" "log/slog" "strconv" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/dogecloud" "github.com/certimate-go/certimate/pkg/core/deployer" dogesdk "github.com/certimate-go/certimate/pkg/sdk3rd/dogecloud" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type DeployerConfig struct { // 多吉云 AccessKey。 AccessKey string `json:"accessKey"` // 多吉云 SecretKey。 SecretKey string `json:"secretKey"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名(不支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *dogesdk.Client sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKey, config.SecretKey) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKey: config.AccessKey, SecretKey: config.SecretKey, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no cdn domains to deploy") } else { d.logger.Info("found cdn domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: certId, _ := strconv.ParseInt(upres.CertId, 10, 64) if err := d.updateDomainCertificate(ctx, domain, certId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 获取域名列表 // REF: https://docs.dogecloud.com/cdn/api-domain-list listCdnDomainResp, err := d.sdkClient.ListCdnDomainWithContext(ctx) d.logger.Debug("sdk request 'cdn.ListCdnDomain'", slog.Any("response", listCdnDomainResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.ListCdnDomain': %w", err) } if listCdnDomainResp.Data != nil { ignoredStatuses := []string{"offline"} for _, domainItem := range listCdnDomainResp.Data.Domains { if lo.Contains(ignoredStatuses, domainItem.Status) { continue } domains = append(domains, domainItem.Name) } } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId int64) error { // 绑定证书 // REF: https://docs.dogecloud.com/cdn/api-cert-bind bindCdnCertReq := &dogesdk.BindCdnCertRequest{ CertId: cloudCertId, Domain: domain, } bindCdnCertResp, err := d.sdkClient.BindCdnCertWithContext(ctx, bindCdnCertReq) d.logger.Debug("sdk request 'cdn.BindCdnCert'", slog.Any("request", bindCdnCertReq), slog.Any("response", bindCdnCertResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'cdn.BindCdnCert': %w", err) } return nil } func createSDKClient(accessKey, secretKey string) (*dogesdk.Client, error) { return dogesdk.NewClient(accessKey, secretKey) } ================================================ FILE: pkg/core/deployer/providers/dogecloud-cdn/dogecloud_cdn_test.go ================================================ package dogecloudcdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/dogecloud-cdn" ) var ( fInputCertPath string fInputKeyPath string fAccessKey string fSecretKey string fDomain string ) func init() { argsPrefix := "DOGECLOUDCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "") flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./dogecloud_cdn_test.go -args \ --DOGECLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --DOGECLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --DOGECLOUDCDN_ACCESSKEY="your-access-key" \ --DOGECLOUDCDN_SECRETKEY="your-secret-key" \ --DOGECLOUDCDN_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEY: %v", fAccessKey), fmt.Sprintf("SECRETKEY: %v", fSecretKey), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKey: fAccessKey, SecretKey: fSecretKey, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/dokploy/dokploy.go ================================================ package dokploy import ( "context" "errors" "fmt" "log/slog" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/dokploy" "github.com/certimate-go/certimate/pkg/core/deployer" ) type DeployerConfig struct { // Dokploy 服务地址。 ServerUrl string `json:"serverUrl"` // Dokploy API Key。 ApiKey string `json:"apiKey"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ ServerUrl: config.ServerUrl, ApiKey: config.ApiKey, AllowInsecureConnections: config.AllowInsecureConnections, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } return &deployer.DeployResult{}, nil } ================================================ FILE: pkg/core/deployer/providers/dokploy/dokploy_test.go ================================================ package dokploy_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/dokploy" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fApiKey string ) func init() { argsPrefix := "DOKPLOY_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") } /* Shell command to run this test: go test -v ./1panel_console_test.go -args \ --DOKPLOY_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --DOKPLOY_INPUTKEYPATH="/path/to/your-input-key.pem" \ --DOKPLOY_SERVERURL="http://127.0.0.1:3000" \ --DOKPLOY_APIKEY="your-api-key" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("APIKEY: %v", fApiKey), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ServerUrl: fServerUrl, ApiKey: fApiKey, AllowInsecureConnections: true, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/flexcdn/consts.go ================================================ package flexcdn const ( // 资源类型:替换指定证书。 RESOURCE_TYPE_CERTIFICATE = "certificate" ) ================================================ FILE: pkg/core/deployer/providers/flexcdn/flexcdn.go ================================================ package flexcdn import ( "context" "crypto/tls" "encoding/base64" "errors" "fmt" "log/slog" "time" "github.com/certimate-go/certimate/pkg/core/deployer" flexcdnsdk "github.com/certimate-go/certimate/pkg/sdk3rd/flexcdn" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type DeployerConfig struct { // FlexCDN 服务地址。 ServerUrl string `json:"serverUrl"` // FlexCDN 用户角色。 // 可取值 "user"、"admin"。 ApiRole string `json:"apiRole"` // FlexCDN AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // FlexCDN AccessKey。 AccessKey string `json:"accessKey"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 证书 ID。 // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。 CertificateId int64 `json:"certificateId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *flexcdnsdk.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ServerUrl, config.ApiRole, config.AccessKeyId, config.AccessKey, config.AllowInsecureConnections) 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) (*deployer.DeployResult, error) { // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_CERTIFICATE: if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.CertificateId == 0 { return errors.New("config `certificateId` is required") } // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return err } // 修改证书 // REF: https://flexcdn.cloud/dev/api/service/SSLCertService?role=user#updateSSLCert updateSSLCertReq := &flexcdnsdk.UpdateSSLCertRequest{ SSLCertId: d.config.CertificateId, IsOn: true, Name: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()), Description: "upload from certimate", ServerName: certX509.Subject.CommonName, IsCA: false, CertData: base64.StdEncoding.EncodeToString([]byte(certPEM)), KeyData: base64.StdEncoding.EncodeToString([]byte(privkeyPEM)), TimeBeginAt: certX509.NotBefore.Unix(), TimeEndAt: certX509.NotAfter.Unix(), DNSNames: certX509.DNSNames, CommonNames: []string{certX509.Subject.CommonName}, } updateSSLCertResp, err := d.sdkClient.UpdateSSLCertWithContext(ctx, updateSSLCertReq) d.logger.Debug("sdk request 'flexcdn.UpdateSSLCert'", slog.Any("request", updateSSLCertReq), slog.Any("response", updateSSLCertResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'flexcdn.UpdateSSLCert': %w", err) } return nil } func createSDKClient(serverUrl, apiRole, accessKeyId, accessKey string, skipTlsVerify bool) (*flexcdnsdk.Client, error) { client, err := flexcdnsdk.NewClient(serverUrl, apiRole, accessKeyId, accessKey) if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } ================================================ FILE: pkg/core/deployer/providers/flexcdn/flexcdn_test.go ================================================ package flexcdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/flexcdn" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fAccessKeyId string fAccessKey string fCertificateId int64 ) func init() { argsPrefix := "FLEXCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "") flag.Int64Var(&fCertificateId, argsPrefix+"CERTIFICATEID", 0, "") } /* Shell command to run this test: go test -v ./flexcdn_test.go -args \ --FLEXCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --FLEXCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --FLEXCDN_SERVERURL="http://127.0.0.1:7788" \ --FLEXCDN_ACCESSKEYID="your-access-key-id" \ --FLEXCDN_ACCESSKEY="your-access-key" \ --FLEXCDN_CERTIFICATEID="your-certificate-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy_ToCertificate", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEY: %v", fAccessKey), fmt.Sprintf("CERTIFICATEID: %v", fCertificateId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ServerUrl: fServerUrl, ApiRole: "user", AccessKeyId: fAccessKeyId, AccessKey: fAccessKey, AllowInsecureConnections: true, ResourceType: provider.RESOURCE_TYPE_CERTIFICATE, CertificateId: fCertificateId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/flyio/flyio.go ================================================ package flyio import ( "context" "errors" "fmt" "log/slog" "github.com/certimate-go/certimate/pkg/core/deployer" flyiosdk "github.com/certimate-go/certimate/pkg/sdk3rd/flyio" ) type DeployerConfig struct { // Fly.io API Token。 ApiToken string `json:"apiToken"` // Fly.io 应用名称。 AppName string `json:"appName"` // 自定义域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *flyiosdk.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("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) (*deployer.DeployResult, error) { if d.config.AppName == "" { return nil, errors.New("config `appName` is required") } if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } // 导入自定义证书 // REF: https://fly.io/docs/machines/api/certificates-resource/#import-custom-certificate importCustomCertificateReq := &flyiosdk.ImportCustomCertificateRequest{ AppName: d.config.AppName, Hostname: d.config.Domain, Fullchain: certPEM, PrivateKey: privkeyPEM, } importCustomCertificateResp, err := d.sdkClient.ImportCustomCertificateWithContext(ctx, importCustomCertificateReq) d.logger.Debug("sdk request 'flyio.ImportCustomCertificate'", slog.Any("request", importCustomCertificateReq), slog.Any("response", importCustomCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'flyio.ImportCustomCertificate': %w", err) } return &deployer.DeployResult{}, nil } func createSDKClient(apiToken string) (*flyiosdk.Client, error) { return flyiosdk.NewClient(apiToken) } ================================================ FILE: pkg/core/deployer/providers/flyio/flyio_test.go ================================================ package flyio_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/flyio" ) var ( fInputCertPath string fInputKeyPath string fApiToken string fAppName string fDomain string ) func init() { argsPrefix := "FLYIO_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "") flag.StringVar(&fAppName, argsPrefix+"APPNAME", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./flyio_test.go -args \ --FLYIO_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --FLYIO_INPUTKEYPATH="/path/to/your-input-key.pem" \ --FLYIO_APITOKEN="your-api-token" \ --FLYIO_APPNAME="your-app-name" \ --FLYIO_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("APITOKEN: %v", fApiToken), fmt.Sprintf("APPNAME: %v", fAppName), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ApiToken: fApiToken, AppName: fAppName, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go ================================================ package gcorecdn import ( "context" "errors" "fmt" "log/slog" "strconv" "github.com/G-Core/gcorelabscdn-go/gcore" "github.com/G-Core/gcorelabscdn-go/gcore/provider" "github.com/G-Core/gcorelabscdn-go/resources" "github.com/G-Core/gcorelabscdn-go/sslcerts" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/gcore-cdn" "github.com/certimate-go/certimate/pkg/core/deployer" gcoresdk "github.com/certimate-go/certimate/pkg/sdk3rd/gcore" ) type DeployerConfig struct { // G-Core API Token。 ApiToken string `json:"apiToken"` // CDN 资源 ID。 ResourceId int64 `json:"resourceId"` // 证书 ID。 // 选填。零值时表示新建证书;否则表示更新证书。 CertificateId int64 `json:"certificateId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClients *wSDKClients sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) type wSDKClients struct { Resources *resources.Service SSLCerts *sslcerts.Service } func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } clients, err := createSDKClients(config.ApiToken) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ ApiToken: config.ApiToken, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClients: clients, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { if d.config.ResourceId == 0 { return nil, errors.New("config `resourceId` is required") } // 如果原证书 ID 为空,则创建证书;否则更新证书。 var cloudCertId int64 if d.config.CertificateId == 0 { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } cloudCertId, _ = strconv.ParseInt(upres.CertId, 10, 64) } else { // 获取证书 // REF: https://api.gcore.com/docs/cdn#tag/SSL-certificates/paths/~1cdn~1sslData~1%7Bssl_id%7D/get getCertificateDetailResp, err := d.sdkClients.SSLCerts.Get(ctx, d.config.CertificateId) d.logger.Debug("sdk request 'sslcerts.Get'", slog.Int64("sslId", d.config.CertificateId), slog.Any("response", getCertificateDetailResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'sslcerts.Get': %w", err) } // 更新证书 // REF: https://api.gcore.com/docs/cdn#tag/SSL-certificates/paths/~1cdn~1sslData~1%7Bssl_id%7D/get changeCertificateReq := &sslcerts.UpdateRequest{ Name: getCertificateDetailResp.Name, Cert: certPEM, PrivateKey: privkeyPEM, ValidateRootCA: false, } changeCertificateResp, err := d.sdkClients.SSLCerts.Update(ctx, getCertificateDetailResp.ID, changeCertificateReq) d.logger.Debug("sdk request 'sslcerts.Update'", slog.Int64("sslId", getCertificateDetailResp.ID), slog.Any("request", changeCertificateReq), slog.Any("response", changeCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'sslcerts.Update': %w", err) } cloudCertId = changeCertificateResp.ID } // 获取 CDN 资源详情 // REF: https://api.gcore.com/docs/cdn#tag/CDN-resources/paths/~1cdn~1resources~1%7Bresource_id%7D/get getResourceResp, err := d.sdkClients.Resources.Get(ctx, d.config.ResourceId) d.logger.Debug("sdk request 'resources.Get'", slog.Any("resourceId", d.config.ResourceId), slog.Any("response", getResourceResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'resources.Get': %w", err) } // 更新 CDN 资源详情 // REF: https://api.gcore.com/docs/cdn#tag/CDN-resources/operation/change_cdn_resource updateResourceReq := &resources.UpdateRequest{ Description: getResourceResp.Description, Active: getResourceResp.Active, OriginGroup: int(getResourceResp.OriginGroup), OriginProtocol: getResourceResp.OriginProtocol, SecondaryHostnames: getResourceResp.SecondaryHostnames, SSlEnabled: true, SSLData: int(cloudCertId), ProxySSLEnabled: getResourceResp.ProxySSLEnabled, Options: &gcore.Options{}, } if getResourceResp.ProxySSLCA != 0 { updateResourceReq.ProxySSLCA = &getResourceResp.ProxySSLCA } if getResourceResp.ProxySSLData != 0 { updateResourceReq.ProxySSLData = &getResourceResp.ProxySSLData } updateResourceResp, err := d.sdkClients.Resources.Update(ctx, d.config.ResourceId, updateResourceReq) d.logger.Debug("sdk request 'resources.Update'", slog.Int64("resourceId", d.config.ResourceId), slog.Any("request", updateResourceReq), slog.Any("response", updateResourceResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'resources.Update': %w", err) } return &deployer.DeployResult{}, nil } func createSDKClients(apiToken string) (*wSDKClients, error) { if apiToken == "" { return nil, errors.New("gcore: invalid api token") } requester := provider.NewClient( gcoresdk.BASE_URL, provider.WithSigner(gcoresdk.NewAuthRequestSigner(apiToken)), ) resourcesSrv := resources.NewService(requester) sslCertsSrv := sslcerts.NewService(requester) return &wSDKClients{ Resources: resourcesSrv, SSLCerts: sslCertsSrv, }, nil } ================================================ FILE: pkg/core/deployer/providers/gcore-cdn/gcore_cdn_test.go ================================================ package gcorecdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/gcore-cdn" ) var ( fInputCertPath string fInputKeyPath string fApiToken string fResourceId int64 ) func init() { argsPrefix := "GCORECDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "") flag.Int64Var(&fResourceId, argsPrefix+"RESOURCEID", 0, "") } /* Shell command to run this test: go test -v ./gcore_cdn_test.go -args \ --GCORECDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --GCORECDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --GCORECDN_APITOKEN="your-api-token" \ --GCORECDN_RESOURCEID="your-cdn-resource-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("APITOKEN: %v", fApiToken), fmt.Sprintf("RESOURCEID: %v", fResourceId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ApiToken: fApiToken, ResourceId: fResourceId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/goedge/consts.go ================================================ package goedge const ( // 资源类型:替换指定证书。 RESOURCE_TYPE_CERTIFICATE = "certificate" ) ================================================ FILE: pkg/core/deployer/providers/goedge/goedge.go ================================================ package goedge import ( "context" "crypto/tls" "encoding/base64" "errors" "fmt" "log/slog" "time" "github.com/certimate-go/certimate/pkg/core/deployer" goedgesdk "github.com/certimate-go/certimate/pkg/sdk3rd/goedge" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type DeployerConfig struct { // GoEdge 服务地址。 ServerUrl string `json:"serverUrl"` // GoEdge 用户角色。 // 可取值 "user"、"admin"。 ApiRole string `json:"apiRole"` // GoEdge AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // GoEdge AccessKey。 AccessKey string `json:"accessKey"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 证书 ID。 // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。 CertificateId int64 `json:"certificateId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *goedgesdk.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ServerUrl, config.ApiRole, config.AccessKeyId, config.AccessKey, config.AllowInsecureConnections) 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) (*deployer.DeployResult, error) { // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_CERTIFICATE: if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.CertificateId == 0 { return errors.New("config `certificateId` is required") } // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return err } // 修改证书 // REF: https://goedge.cloud/dev/api/service/SSLCertService?role=user#updateSSLCert updateSSLCertReq := &goedgesdk.UpdateSSLCertRequest{ SSLCertId: d.config.CertificateId, IsOn: true, Name: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()), Description: "upload from certimate", ServerName: certX509.Subject.CommonName, IsCA: false, CertData: base64.StdEncoding.EncodeToString([]byte(certPEM)), KeyData: base64.StdEncoding.EncodeToString([]byte(privkeyPEM)), TimeBeginAt: certX509.NotBefore.Unix(), TimeEndAt: certX509.NotAfter.Unix(), DNSNames: certX509.DNSNames, CommonNames: []string{certX509.Subject.CommonName}, } updateSSLCertResp, err := d.sdkClient.UpdateSSLCertWithContext(ctx, updateSSLCertReq) d.logger.Debug("sdk request 'goedge.UpdateSSLCert'", slog.Any("request", updateSSLCertReq), slog.Any("response", updateSSLCertResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'goedge.UpdateSSLCert': %w", err) } return nil } func createSDKClient(serverUrl, apiRole, accessKeyId, accessKey string, skipTlsVerify bool) (*goedgesdk.Client, error) { client, err := goedgesdk.NewClient(serverUrl, apiRole, accessKeyId, accessKey) if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } ================================================ FILE: pkg/core/deployer/providers/goedge/goedge_test.go ================================================ package goedge_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/goedge" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fAccessKeyId string fAccessKey string fCertificateId int64 ) func init() { argsPrefix := "GOEDGE_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "") flag.Int64Var(&fCertificateId, argsPrefix+"CERTIFICATEID", 0, "") } /* Shell command to run this test: go test -v ./goedge_test.go -args \ --GOEDGE_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --GOEDGE_INPUTKEYPATH="/path/to/your-input-key.pem" \ --GOEDGE_SERVERURL="http://127.0.0.1:7788" \ --GOEDGE_ACCESSKEYID="your-access-key-id" \ --GOEDGE_ACCESSKEY="your-access-key" \ --GOEDGE_CERTIFICATEID="your-certificate-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy_ToCertificate", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEY: %v", fAccessKey), fmt.Sprintf("CERTIFICATEID: %v", fCertificateId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ServerUrl: fServerUrl, ApiRole: "user", AccessKeyId: fAccessKeyId, AccessKey: fAccessKey, AllowInsecureConnections: true, ResourceType: provider.RESOURCE_TYPE_CERTIFICATE, CertificateId: fCertificateId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/huaweicloud-cdn/consts.go ================================================ package huaweicloudcdn const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go ================================================ package huaweicloudcdn import ( "context" "errors" "fmt" "log/slog" "strings" "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global" hccdn "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2" hccdnmodel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/model" hccdnregion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/region" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/huaweicloud-scm" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-cdn/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 华为云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // 华为云企业项目 ID。 EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` // 华为云区域。 Region string `json:"region"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.CdnClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient( config.AccessKeyId, config.SecretAccessKey, config.Region, ) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, SecretAccessKey: config.SecretAccessKey, EnterpriseProjectId: config.EnterpriseProjectId, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return xcerthostname.IsMatch(d.config.Domain, domain) }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by wildcard") } } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no cdn domains to deploy") } else { d.logger.Info("found cdn domains to deploy", slog.Any("domains", domains)) var errs []error const MAX_DOMAIN_PER_REQUEST = 50 domainChunks := lo.Chunk(domains, MAX_DOMAIN_PER_REQUEST) for _, domains := range domainChunks { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainsCertificate(ctx, domains, upres.CertId, upres.CertName); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 查询域名列表 // REF: https://support.huaweicloud.com/api-cdn/ListDomains.html listDomainsPageNumber := 1 listDomainsPageSize := 100 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } listDomainsReq := &hccdnmodel.ListDomainsRequest{ EnterpriseProjectId: lo.EmptyableToPtr(d.config.EnterpriseProjectId), PageNumber: lo.ToPtr(int32(listDomainsPageNumber)), PageSize: lo.ToPtr(int32(listDomainsPageSize)), } listDomainsResp, err := d.sdkClient.ListDomains(listDomainsReq) d.logger.Debug("sdk request 'cdn.ListDomains'", slog.Any("request", listDomainsReq), slog.Any("response", listDomainsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.ListDomains': %w", err) } if listDomainsResp.Domains == nil { break } ignoredStatuses := []string{"offline", "checking", "check_failed", "deleting"} for _, domainItem := range *listDomainsResp.Domains { if lo.Contains(ignoredStatuses, lo.FromPtr(domainItem.DomainStatus)) { continue } domains = append(domains, lo.FromPtr(domainItem.DomainName)) } if len(*listDomainsResp.Domains) < listDomainsPageSize { break } listDomainsPageNumber++ } return domains, nil } func (d *Deployer) updateDomainsCertificate(ctx context.Context, domains []string, cloudCertId, cloudCertName string) error { // 更新加速域名配置 // REF: https://support.huaweicloud.com/api-cdn/UpdateDomainMultiCertificates.html // REF: https://support.huaweicloud.com/usermanual-cdn/cdn_01_0306.html updateDomainMultiCertificatesReq := &hccdnmodel.UpdateDomainMultiCertificatesRequest{ EnterpriseProjectId: lo.EmptyableToPtr(d.config.EnterpriseProjectId), Body: &hccdnmodel.UpdateDomainMultiCertificatesRequestBody{ Https: &hccdnmodel.UpdateDomainMultiCertificatesRequestBodyContent{ DomainName: strings.Join(domains, ","), HttpsSwitch: 1, CertificateType: lo.ToPtr(int32(2)), ScmCertificateId: lo.ToPtr(cloudCertId), CertName: lo.ToPtr(cloudCertName), }, }, } updateDomainMultiCertificatesResp, err := d.sdkClient.UpdateDomainMultiCertificates(updateDomainMultiCertificatesReq) d.logger.Debug("sdk request 'cdn.UpdateDomainMultiCertificates'", slog.Any("request", updateDomainMultiCertificatesReq), slog.Any("response", updateDomainMultiCertificatesResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'cdn.UpdateDomainMultiCertificates': %w", err) } return nil } func createSDKClient(accessKeyId, secretAccessKey, region string) (*internal.CdnClient, error) { if region == "" { region = "cn-north-1" // CDN 服务默认区域:华北一北京 } auth, err := global.NewCredentialsBuilder(). WithAk(accessKeyId). WithSk(secretAccessKey). SafeBuild() if err != nil { return nil, err } hcRegion, err := hccdnregion.SafeValueOf(region) if err != nil { return nil, err } hcClient, err := hccdn.CdnClientBuilder(). WithRegion(hcRegion). WithCredential(auth). SafeBuild() if err != nil { return nil, err } client := internal.NewCdnClient(hcClient) return client, nil } ================================================ FILE: pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn_test.go ================================================ package huaweicloudcdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-cdn" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string fRegion string fDomain string ) func init() { argsPrefix := "HUAWEICLOUDCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./huaweicloud_cdn_test.go -args \ --HUAWEICLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --HUAWEICLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --HUAWEICLOUDCDN_ACCESSKEYID="your-access-key-id" \ --HUAWEICLOUDCDN_SECRETACCESSKEY="your-secret-access-key" \ --HUAWEICLOUDCDN_REGION="cn-north-1" \ --HUAWEICLOUDCDN_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, Region: fRegion, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/huaweicloud-cdn/internal/client.go ================================================ package internal import ( httpclient "github.com/huaweicloud/huaweicloud-sdk-go-v3/core" hwcdn "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2" "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/model" ) // This is a partial copy of https://github.com/huaweicloud/huaweicloud-sdk-go-v3/blob/master/services/cdn/v2/cdn_client.go // to lightweight the vendor packages in the built binary. type CdnClient struct { HcClient *httpclient.HcHttpClient } func NewCdnClient(hcClient *httpclient.HcHttpClient) *CdnClient { return &CdnClient{HcClient: hcClient} } func (c *CdnClient) ListDomains(request *model.ListDomainsRequest) (*model.ListDomainsResponse, error) { requestDef := hwcdn.GenReqDefForListDomains() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.ListDomainsResponse), nil } } func (c *CdnClient) UpdateDomainMultiCertificates(request *model.UpdateDomainMultiCertificatesRequest) (*model.UpdateDomainMultiCertificatesResponse, error) { requestDef := hwcdn.GenReqDefForUpdateDomainMultiCertificates() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.UpdateDomainMultiCertificatesResponse), nil } } ================================================ FILE: pkg/core/deployer/providers/huaweicloud-elb/consts.go ================================================ package huaweicloudelb const ( // 资源类型:替换指定证书。 RESOURCE_TYPE_CERTIFICATE = "certificate" // 资源类型:部署到指定负载均衡器。 RESOURCE_TYPE_LOADBALANCER = "loadbalancer" // 资源类型:部署到指定监听器。 RESOURCE_TYPE_LISTENER = "listener" ) ================================================ FILE: pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go ================================================ package huaweicloudelb import ( "context" "errors" "fmt" "log/slog" "strings" "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic" "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global" hcelb "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3" hcelbmodel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/model" hcelbregion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/region" hciam "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3" hciammodel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model" hciamregion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/huaweicloud-elb" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-elb/internal" ) type DeployerConfig struct { // 华为云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // 华为云企业项目 ID。 EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` // 华为云区域。 Region string `json:"region"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 证书 ID。 // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。 CertificateId string `json:"certificateId,omitempty"` // 负载均衡器 ID。 // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER] 时必填。 LoadbalancerId string `json:"loadbalancerId,omitempty"` // 负载均衡监听 ID。 // 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。 ListenerId string `json:"listenerId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.ElbClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, SecretAccessKey: config.SecretAccessKey, EnterpriseProjectId: config.EnterpriseProjectId, Region: config.Region, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_CERTIFICATE: if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil { return nil, err } case RESOURCE_TYPE_LOADBALANCER: if err := d.deployToLoadbalancer(ctx, certPEM, privkeyPEM); err != nil { return nil, err } case RESOURCE_TYPE_LISTENER: if err := d.deployToListener(ctx, certPEM, privkeyPEM); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToLoadbalancer(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.LoadbalancerId == "" { return errors.New("config `loadbalancerId` is required") } // 查询负载均衡器详情 // REF: https://support.huaweicloud.com/api-elb/ShowLoadBalancer.html showLoadBalancerReq := &hcelbmodel.ShowLoadBalancerRequest{ LoadbalancerId: d.config.LoadbalancerId, } showLoadBalancerResp, err := d.sdkClient.ShowLoadBalancer(showLoadBalancerReq) d.logger.Debug("sdk request 'elb.ShowLoadBalancer'", slog.Any("request", showLoadBalancerReq), slog.Any("response", showLoadBalancerResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'elb.ShowLoadBalancer': %w", err) } // 查询监听器列表 // REF: https://support.huaweicloud.com/api-elb/ListListeners.html listenerIds := make([]string, 0) listListenersMarker := (*string)(nil) for { select { case <-ctx.Done(): return ctx.Err() default: } listListenersReq := &hcelbmodel.ListListenersRequest{ Marker: listListenersMarker, Limit: lo.ToPtr(int32(2000)), Protocol: &[]string{"HTTPS", "TERMINATED_HTTPS"}, LoadbalancerId: &[]string{showLoadBalancerResp.Loadbalancer.Id}, } if d.config.EnterpriseProjectId != "" { listListenersReq.EnterpriseProjectId = lo.ToPtr([]string{d.config.EnterpriseProjectId}) } listListenersResp, err := d.sdkClient.ListListeners(listListenersReq) d.logger.Debug("sdk request 'elb.ListListeners'", slog.Any("request", listListenersReq), slog.Any("response", listListenersResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'elb.ListListeners': %w", err) } if listListenersResp.Listeners == nil { break } for _, listener := range *listListenersResp.Listeners { listenerIds = append(listenerIds, listener.Id) } if len(*listListenersResp.Listeners) == 0 || listListenersResp.PageInfo.NextMarker == nil { break } listListenersMarker = listListenersResp.PageInfo.NextMarker } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 遍历更新监听器证书 if len(listenerIds) == 0 { d.logger.Info("no listeners to deploy") } else { d.logger.Info("found https listeners to deploy", slog.Any("listenerIds", listenerIds)) var errs []error for _, listenerId := range listenerIds { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateListenerCertificate(ctx, listenerId, upres.CertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) deployToListener(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.ListenerId == "" { return errors.New("config `listenerId` is required") } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 更新监听器证书 if err := d.updateListenerCertificate(ctx, d.config.ListenerId, upres.CertId); err != nil { return err } return nil } func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.CertificateId == "" { return errors.New("config `certificateId` is required") } // 替换证书 opres, err := d.sdkCertmgr.Replace(ctx, d.config.CertificateId, certPEM, privkeyPEM) if err != nil { return fmt.Errorf("failed to replace certificate file: %w", err) } else { d.logger.Info("ssl certificate replaced", slog.Any("result", opres)) } return nil } func (d *Deployer) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error { // 查询监听器详情 // REF: https://support.huaweicloud.com/api-elb/ShowListener.html showListenerReq := &hcelbmodel.ShowListenerRequest{ ListenerId: cloudListenerId, } showListenerResp, err := d.sdkClient.ShowListener(showListenerReq) d.logger.Debug("sdk request 'elb.ShowListener'", slog.Any("request", showListenerReq), slog.Any("response", showListenerResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'elb.ShowListener': %w", err) } // 更新监听器 // REF: https://support.huaweicloud.com/api-elb/UpdateListener.html updateListenerReq := &hcelbmodel.UpdateListenerRequest{ ListenerId: cloudListenerId, Body: &hcelbmodel.UpdateListenerRequestBody{ Listener: &hcelbmodel.UpdateListenerOption{ DefaultTlsContainerRef: lo.ToPtr(cloudCertId), }, }, } if showListenerResp.Listener.SniContainerRefs != nil { if len(showListenerResp.Listener.SniContainerRefs) > 0 { // 如果开启 SNI,需替换同 SAN 的证书 sniCertIds := make([]string, 0) sniCertIds = append(sniCertIds, cloudCertId) listOldCertificateReq := &hcelbmodel.ListCertificatesRequest{ Id: &showListenerResp.Listener.SniContainerRefs, } listOldCertificateResp, err := d.sdkClient.ListCertificates(listOldCertificateReq) d.logger.Debug("sdk request 'elb.ListCertificates'", slog.Any("request", listOldCertificateReq), slog.Any("response", listOldCertificateResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'elb.ListCertificates': %w", err) } showNewCertificateReq := &hcelbmodel.ShowCertificateRequest{ CertificateId: cloudCertId, } showNewCertificateResp, err := d.sdkClient.ShowCertificate(showNewCertificateReq) d.logger.Debug("sdk request 'elb.ShowCertificate'", slog.Any("request", showNewCertificateReq), slog.Any("response", showNewCertificateResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'elb.ShowCertificate': %w", err) } for _, oldCertInfo := range *listOldCertificateResp.Certificates { newCertInfo := showNewCertificateResp.Certificate if oldCertInfo.SubjectAlternativeNames != nil && newCertInfo.SubjectAlternativeNames != nil { if strings.Join(*oldCertInfo.SubjectAlternativeNames, ",") == strings.Join(*newCertInfo.SubjectAlternativeNames, ",") { continue } } else { if oldCertInfo.Domain == newCertInfo.Domain { continue } } sniCertIds = append(sniCertIds, oldCertInfo.Id) } updateListenerReq.Body.Listener.SniContainerRefs = &sniCertIds } if showListenerResp.Listener.SniMatchAlgo != "" { updateListenerReq.Body.Listener.SniMatchAlgo = lo.ToPtr(showListenerResp.Listener.SniMatchAlgo) } } updateListenerResp, err := d.sdkClient.UpdateListener(updateListenerReq) d.logger.Debug("sdk request 'elb.UpdateListener'", slog.Any("request", updateListenerReq), slog.Any("response", updateListenerResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'elb.UpdateListener': %w", err) } return nil } func createSDKClient(accessKeyId, secretAccessKey, region string) (*internal.ElbClient, error) { projectId, err := getSDKProjectId(accessKeyId, secretAccessKey, region) if err != nil { return nil, err } auth, err := basic.NewCredentialsBuilder(). WithAk(accessKeyId). WithSk(secretAccessKey). WithProjectId(projectId). SafeBuild() if err != nil { return nil, err } hcRegion, err := hcelbregion.SafeValueOf(region) if err != nil { return nil, err } hcClient, err := hcelb.ElbClientBuilder(). WithRegion(hcRegion). WithCredential(auth). SafeBuild() if err != nil { return nil, err } client := internal.NewElbClient(hcClient) return client, nil } func getSDKProjectId(accessKeyId, secretAccessKey, region string) (string, error) { if region == "" { region = "cn-north-4" // IAM 服务默认区域:华北四北京 } auth, err := global.NewCredentialsBuilder(). WithAk(accessKeyId). WithSk(secretAccessKey). SafeBuild() if err != nil { return "", err } hcRegion, err := hciamregion.SafeValueOf(region) if err != nil { return "", err } hcClient, err := hciam.IamClientBuilder(). WithRegion(hcRegion). WithCredential(auth). SafeBuild() if err != nil { return "", err } client := hciam.NewIamClient(hcClient) request := &hciammodel.KeystoneListProjectsRequest{ Name: ®ion, } response, err := client.KeystoneListProjects(request) if err != nil { return "", err } else if response.Projects == nil || len(*response.Projects) == 0 { return "", errors.New("huaweicloud: no project found") } return (*response.Projects)[0].Id, nil } ================================================ FILE: pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb_test.go ================================================ package huaweicloudelb_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-elb" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string fRegion string fCertificateId string fLoadbalancerId string fListenerId string ) func init() { argsPrefix := "HUAWEICLOUDELB_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "") flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "") flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "") } /* Shell command to run this test: go test -v ./huaweicloud_elb_test.go -args \ --HUAWEICLOUDELB_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --HUAWEICLOUDELB_INPUTKEYPATH="/path/to/your-input-key.pem" \ --HUAWEICLOUDELB_ACCESSKEYID="your-access-key-id" \ --HUAWEICLOUDELB_SECRETACCESSKEY="your-secret-access-key" \ --HUAWEICLOUDELB_REGION="cn-north-1" \ --HUAWEICLOUDELB_CERTIFICATEID="your-elb-cert-id" \ --HUAWEICLOUDELB_LOADBALANCERID="your-elb-loadbalancer-id" \ --HUAWEICLOUDELB_LISTENERID="your-elb-listener-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy_ToCertificate", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("CERTIFICATEID: %v", fCertificateId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, Region: fRegion, ResourceType: provider.RESOURCE_TYPE_CERTIFICATE, CertificateId: fCertificateId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) t.Run("Deploy_ToLoadbalancer", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, Region: fRegion, ResourceType: provider.RESOURCE_TYPE_LOADBALANCER, LoadbalancerId: fLoadbalancerId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) t.Run("Deploy_ToListenerId", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("LISTENERID: %v", fListenerId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, Region: fRegion, ResourceType: provider.RESOURCE_TYPE_LISTENER, ListenerId: fListenerId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/huaweicloud-elb/internal/client.go ================================================ package internal import ( httpclient "github.com/huaweicloud/huaweicloud-sdk-go-v3/core" hwelb "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3" "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/model" ) // This is a partial copy of https://github.com/huaweicloud/huaweicloud-sdk-go-v3/blob/master/services/elb/v3/elb_client.go // to lightweight the vendor packages in the built binary. type ElbClient struct { HcClient *httpclient.HcHttpClient } func NewElbClient(hcClient *httpclient.HcHttpClient) *ElbClient { return &ElbClient{HcClient: hcClient} } func (c *ElbClient) ListCertificates(request *model.ListCertificatesRequest) (*model.ListCertificatesResponse, error) { requestDef := hwelb.GenReqDefForListCertificates() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.ListCertificatesResponse), nil } } func (c *ElbClient) ListListeners(request *model.ListListenersRequest) (*model.ListListenersResponse, error) { requestDef := hwelb.GenReqDefForListListeners() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.ListListenersResponse), nil } } func (c *ElbClient) ShowCertificate(request *model.ShowCertificateRequest) (*model.ShowCertificateResponse, error) { requestDef := hwelb.GenReqDefForShowCertificate() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.ShowCertificateResponse), nil } } func (c *ElbClient) ShowListener(request *model.ShowListenerRequest) (*model.ShowListenerResponse, error) { requestDef := hwelb.GenReqDefForShowListener() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.ShowListenerResponse), nil } } func (c *ElbClient) ShowLoadBalancer(request *model.ShowLoadBalancerRequest) (*model.ShowLoadBalancerResponse, error) { requestDef := hwelb.GenReqDefForShowLoadBalancer() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.ShowLoadBalancerResponse), nil } } func (c *ElbClient) UpdateListener(request *model.UpdateListenerRequest) (*model.UpdateListenerResponse, error) { requestDef := hwelb.GenReqDefForUpdateListener() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.UpdateListenerResponse), nil } } ================================================ FILE: pkg/core/deployer/providers/huaweicloud-obs/huaweicloud_obs.go ================================================ package huaweicloudobs import ( "bytes" "context" "crypto/hmac" "crypto/md5" "crypto/sha1" "encoding/base64" "errors" "fmt" "log/slog" "net/http" "time" "github.com/certimate-go/certimate/pkg/core/deployer" ) type DeployerConfig struct { // 华为云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // 华为云区域。 Region string `json:"region"` // 存储桶名。 Bucket string `json:"bucket"` // 自定义域名(不支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } return &Deployer{ config: config, logger: slog.Default(), }, 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) (*deployer.DeployResult, error) { if d.config.Region == "" { return nil, fmt.Errorf("config `region` is required") } if d.config.Bucket == "" { return nil, fmt.Errorf("config `bucket` is required") } if d.config.Domain == "" { return nil, fmt.Errorf("config `domain` is required") } // REF: https://support.huaweicloud.com/usermanual-obs/obs_06_3200.html // REF: https://support.huaweicloud.com/api-obs/obs_04_0059.html url := fmt.Sprintf("https://%s.obs.%s.myhuaweicloud.com/?customdomain=%s", d.config.Bucket, d.config.Region, d.config.Domain) bodyXML := fmt.Sprintf(` %s %s %s %s `, d.config.Bucket+"_"+d.config.Domain, certPEM, certPEM, privkeyPEM, ) // 计算 Content-MD5(Base64 编码) md5sum := md5.Sum([]byte(bodyXML)) md5sumEncoded := base64.StdEncoding.EncodeToString(md5sum[:]) // 构造签名字符串 date := time.Now().UTC().Format(http.TimeFormat) method := "PUT" contentType := "application/xml" canonicalizedResource := fmt.Sprintf("/%s/?customdomain=%s", d.config.Bucket, d.config.Domain) stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s\n%s", method, md5sumEncoded, contentType, date, canonicalizedResource) // HMAC-SHA1 签名 h := hmac.New(sha1.New, []byte(d.config.SecretAccessKey)) h.Write([]byte(stringToSign)) signature := base64.StdEncoding.EncodeToString(h.Sum(nil)) // Authorization authHeader := fmt.Sprintf("OBS %s:%s", d.config.AccessKeyId, signature) // 创建请求 req, err := http.NewRequest(method, url, bytes.NewBuffer([]byte(bodyXML))) if err != nil { return nil, fmt.Errorf("huaweicloud obs api error: %w", err) } req.Header.Set("Date", date) req.Header.Set("Authorization", authHeader) req.Header.Set("Content-MD5", md5sumEncoded) req.Header.Set("Content-Type", contentType) // 请求 resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("huaweicloud obs api error: %w", err) } defer resp.Body.Close() // 响应 if resp.StatusCode != http.StatusOK { body := &bytes.Buffer{} body.ReadFrom(resp.Body) return nil, fmt.Errorf("huaweicloud obs api error: unexpected status code: %d (resp: %s)", resp.StatusCode, body.String()) } return &deployer.DeployResult{}, nil } ================================================ FILE: pkg/core/deployer/providers/huaweicloud-obs/huaweicloud_obs_test.go ================================================ package huaweicloudobs_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-obs" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string fRegion string fBucket string fDomain string ) func init() { argsPrefix := "HUAWEICLOUDOBS_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fBucket, argsPrefix+"BUCKET", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./huaweicloud_obs_test.go -args \ --HUAWEICLOUDOBS_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --HUAWEICLOUDOBS_INPUTKEYPATH="/path/to/your-input-key.pem" \ --HUAWEICLOUDOBS_ACCESSKEYID="your-access-key-id" \ --HUAWEICLOUDOBS_SECRETACCESSKEY="your-secret-access-key" \ --HUAWEICLOUDOBS_REGION="cn-north-4" \ --HUAWEICLOUDOBS_BUCKET="your-bucket" \ --HUAWEICLOUDOBS_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("BUCKET: %v", fBucket), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, Region: fRegion, Bucket: fBucket, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/huaweicloud-scm/huaweicloud_scm.go ================================================ package huaweicloudscm import ( "context" "errors" "fmt" "log/slog" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/huaweicloud-scm" "github.com/certimate-go/certimate/pkg/core/deployer" ) type DeployerConfig struct { // 华为云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // 华为云企业项目 ID。 EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, SecretAccessKey: config.SecretAccessKey, EnterpriseProjectId: config.EnterpriseProjectId, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } return &deployer.DeployResult{}, nil } ================================================ FILE: pkg/core/deployer/providers/huaweicloud-waf/consts.go ================================================ package huaweicloudwaf const ( // 资源类型:部署到云模式防护网站。 RESOURCE_TYPE_CLOUDSERVER = "cloudserver" // 资源类型:部署到独享模式防护网站。 RESOURCE_TYPE_PREMIUMHOST = "premiumhost" // 资源类型:替换指定证书。 RESOURCE_TYPE_CERTIFICATE = "certificate" ) ================================================ FILE: pkg/core/deployer/providers/huaweicloud-waf/huaweicloud_waf.go ================================================ package huaweicloudwaf import ( "context" "errors" "fmt" "log/slog" "strings" "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic" "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global" hciam "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3" hciamModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model" hciamregion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region" hcwaf "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1" hcwafmodel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1/model" hcwafregion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1/region" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/huaweicloud-waf" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-waf/internal" ) type DeployerConfig struct { // 华为云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // 华为云企业项目 ID。 EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` // 华为云区域。 Region string `json:"region"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 证书 ID。 // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。 CertificateId string `json:"certificateId,omitempty"` // 防护域名(支持泛域名)。 // 部署资源类型为 [RESOURCE_TYPE_CLOUDSERVER]、[RESOURCE_TYPE_PREMIUMHOST] 时必填。 Domain string `json:"domain,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.WafClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, SecretAccessKey: config.SecretAccessKey, EnterpriseProjectId: config.EnterpriseProjectId, Region: config.Region, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_CLOUDSERVER: if err := d.deployToCloudServer(ctx, certPEM, privkeyPEM); err != nil { return nil, err } case RESOURCE_TYPE_PREMIUMHOST: if err := d.deployToPremiumHost(ctx, certPEM, privkeyPEM); err != nil { return nil, err } case RESOURCE_TYPE_CERTIFICATE: if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.CertificateId == "" { return errors.New("config `certificateId` is required") } // 查询证书 // REF: https://support.huaweicloud.com/api-waf/ShowCertificate.html showCertificateReq := &hcwafmodel.ShowCertificateRequest{ EnterpriseProjectId: lo.EmptyableToPtr(d.config.EnterpriseProjectId), CertificateId: d.config.CertificateId, } showCertificateResp, err := d.sdkClient.ShowCertificate(showCertificateReq) d.logger.Debug("sdk request 'waf.ShowCertificate'", slog.Any("request", showCertificateReq), slog.Any("response", showCertificateResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'waf.ShowCertificate': %w", err) } // 更新证书 // REF: https://support.huaweicloud.com/api-waf/UpdateCertificate.html updateCertificateReq := &hcwafmodel.UpdateCertificateRequest{ EnterpriseProjectId: lo.EmptyableToPtr(d.config.EnterpriseProjectId), CertificateId: d.config.CertificateId, Body: &hcwafmodel.UpdateCertificateRequestBody{ Name: *showCertificateResp.Name, Content: lo.ToPtr(certPEM), Key: lo.ToPtr(privkeyPEM), }, } updateCertificateResp, err := d.sdkClient.UpdateCertificate(updateCertificateReq) d.logger.Debug("sdk request 'waf.UpdateCertificate'", slog.Any("request", updateCertificateReq), slog.Any("response", updateCertificateResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'waf.UpdateCertificate': %w", err) } return nil } func (d *Deployer) deployToCloudServer(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.Domain == "" { return errors.New("config `domain` is required") } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 查询云模式防护域名列表,获取防护域名 ID // REF: https://support.huaweicloud.com/api-waf/ListHost.html hostId := "" listHostPage := 1 listHostPageSize := 100 for { select { case <-ctx.Done(): return ctx.Err() default: } listHostReq := &hcwafmodel.ListHostRequest{ EnterpriseProjectId: lo.EmptyableToPtr(d.config.EnterpriseProjectId), Hostname: lo.ToPtr(strings.TrimPrefix(d.config.Domain, "*")), Page: lo.ToPtr(int32(listHostPage)), Pagesize: lo.ToPtr(int32(listHostPageSize)), } listHostResp, err := d.sdkClient.ListHost(listHostReq) d.logger.Debug("sdk request 'waf.ListHost'", slog.Any("request", listHostReq), slog.Any("response", listHostResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'waf.ListHost': %w", err) } if listHostResp.Items == nil { break } for _, hostItem := range *listHostResp.Items { if strings.TrimPrefix(d.config.Domain, "*") == *hostItem.Hostname { hostId = *hostItem.Id break } } if len(*listHostResp.Items) < listHostPageSize { break } listHostPage++ } if hostId == "" { return fmt.Errorf("could not find cloudserver host '%s'", d.config.Domain) } // 更新云模式防护域名的配置 // REF: https://support.huaweicloud.com/api-waf/UpdateHost.html updateHostReq := &hcwafmodel.UpdateHostRequest{ EnterpriseProjectId: lo.EmptyableToPtr(d.config.EnterpriseProjectId), InstanceId: hostId, Body: &hcwafmodel.UpdateHostRequestBody{ Certificateid: lo.ToPtr(upres.CertId), Certificatename: lo.ToPtr(upres.CertName), }, } updateHostResp, err := d.sdkClient.UpdateHost(updateHostReq) d.logger.Debug("sdk request 'waf.UpdateHost'", slog.Any("request", updateHostReq), slog.Any("response", updateHostResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'waf.UpdateHost': %w", err) } return nil } func (d *Deployer) deployToPremiumHost(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.Domain == "" { return errors.New("config `domain` is required") } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 查询独享模式域名列表,获取防护域名 ID // REF: https://support.huaweicloud.com/api-waf/ListPremiumHost.html var hostId string listPremiumHostPage := 1 listPremiumHostPageSize := 100 for { select { case <-ctx.Done(): return ctx.Err() default: } listPremiumHostReq := &hcwafmodel.ListPremiumHostRequest{ EnterpriseProjectId: lo.EmptyableToPtr(d.config.EnterpriseProjectId), Hostname: lo.ToPtr(strings.TrimPrefix(d.config.Domain, "*")), Page: lo.ToPtr(fmt.Sprintf("%d", listPremiumHostPage)), Pagesize: lo.ToPtr(fmt.Sprintf("%d", listPremiumHostPageSize)), } listPremiumHostResp, err := d.sdkClient.ListPremiumHost(listPremiumHostReq) d.logger.Debug("sdk request 'waf.ListPremiumHost'", slog.Any("request", listPremiumHostReq), slog.Any("response", listPremiumHostResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'waf.ListPremiumHost': %w", err) } if listPremiumHostResp.Items == nil { break } for _, hostItem := range *listPremiumHostResp.Items { if strings.TrimPrefix(d.config.Domain, "*") == *hostItem.Hostname { hostId = *hostItem.Id break } } if len(*listPremiumHostResp.Items) < listPremiumHostPageSize { break } listPremiumHostPage++ } if hostId == "" { return fmt.Errorf("could not find premium host '%s'", d.config.Domain) } // 修改独享模式域名配置 // REF: https://support.huaweicloud.com/api-waf/UpdatePremiumHost.html updatePremiumHostReq := &hcwafmodel.UpdatePremiumHostRequest{ EnterpriseProjectId: lo.EmptyableToPtr(d.config.EnterpriseProjectId), HostId: hostId, Body: &hcwafmodel.UpdatePremiumHostRequestBody{ Certificateid: lo.ToPtr(upres.CertId), Certificatename: lo.ToPtr(upres.CertName), }, } updatePremiumHostResp, err := d.sdkClient.UpdatePremiumHost(updatePremiumHostReq) d.logger.Debug("sdk request 'waf.UpdatePremiumHost'", slog.Any("request", updatePremiumHostReq), slog.Any("response", updatePremiumHostResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'waf.UpdatePremiumHost': %w", err) } return nil } func createSDKClient(accessKeyId, secretAccessKey, region string) (*internal.WafClient, error) { projectId, err := getSDKProjectId(accessKeyId, secretAccessKey, region) if err != nil { return nil, err } auth, err := basic.NewCredentialsBuilder(). WithAk(accessKeyId). WithSk(secretAccessKey). WithProjectId(projectId). SafeBuild() if err != nil { return nil, err } hcRegion, err := hcwafregion.SafeValueOf(region) if err != nil { return nil, err } hcClient, err := hcwaf.WafClientBuilder(). WithRegion(hcRegion). WithCredential(auth). SafeBuild() if err != nil { return nil, err } client := internal.NewWafClient(hcClient) return client, nil } func getSDKProjectId(accessKeyId, secretAccessKey, region string) (string, error) { auth, err := global.NewCredentialsBuilder(). WithAk(accessKeyId). WithSk(secretAccessKey). SafeBuild() if err != nil { return "", err } hcRegion, err := hciamregion.SafeValueOf(region) if err != nil { return "", err } hcClient, err := hciam.IamClientBuilder(). WithRegion(hcRegion). WithCredential(auth). SafeBuild() if err != nil { return "", err } client := hciam.NewIamClient(hcClient) request := &hciamModel.KeystoneListProjectsRequest{ Name: ®ion, } response, err := client.KeystoneListProjects(request) if err != nil { return "", err } else if response.Projects == nil || len(*response.Projects) == 0 { return "", errors.New("huaweicloud: no project found") } return (*response.Projects)[0].Id, nil } ================================================ FILE: pkg/core/deployer/providers/huaweicloud-waf/huaweicloud_waf_test.go ================================================ package huaweicloudwaf_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/huaweicloud-waf" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string fRegion string fResourceType string fDomain string ) func init() { argsPrefix := "HUAWEICLOUDWAF_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fResourceType, argsPrefix+"RESOURCETYPE", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./huaweicloud_waf_test.go -args \ --HUAWEICLOUDWAF_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --HUAWEICLOUDWAF_INPUTKEYPATH="/path/to/your-input-key.pem" \ --HUAWEICLOUDWAF_ACCESSKEYID="your-access-key-id" \ --HUAWEICLOUDWAF_SECRETACCESSKEY="your-secret-access-key" \ --HUAWEICLOUDWAF_REGION="cn-north-1" \ --HUAWEICLOUDWAF_RESOURCETYPE="premium" \ --HUAWEICLOUDWAF_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("RESOURCETYPE: %v", fResourceType), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, Region: fRegion, ResourceType: fResourceType, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/huaweicloud-waf/internal/client.go ================================================ package internal import ( httpclient "github.com/huaweicloud/huaweicloud-sdk-go-v3/core" hwwaf "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1" "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1/model" ) // This is a partial copy of https://github.com/huaweicloud/huaweicloud-sdk-go-v3/blob/master/services/waf/v1/waf_client.go // to lightweight the vendor packages in the built binary. type WafClient struct { HcClient *httpclient.HcHttpClient } func NewWafClient(hcClient *httpclient.HcHttpClient) *WafClient { return &WafClient{HcClient: hcClient} } func (c *WafClient) ListHost(request *model.ListHostRequest) (*model.ListHostResponse, error) { requestDef := hwwaf.GenReqDefForListHost() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.ListHostResponse), nil } } func (c *WafClient) ListPremiumHost(request *model.ListPremiumHostRequest) (*model.ListPremiumHostResponse, error) { requestDef := hwwaf.GenReqDefForListPremiumHost() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.ListPremiumHostResponse), nil } } func (c *WafClient) ShowCertificate(request *model.ShowCertificateRequest) (*model.ShowCertificateResponse, error) { requestDef := hwwaf.GenReqDefForShowCertificate() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.ShowCertificateResponse), nil } } func (c *WafClient) UpdateCertificate(request *model.UpdateCertificateRequest) (*model.UpdateCertificateResponse, error) { requestDef := hwwaf.GenReqDefForUpdateCertificate() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.UpdateCertificateResponse), nil } } func (c *WafClient) UpdateHost(request *model.UpdateHostRequest) (*model.UpdateHostResponse, error) { requestDef := hwwaf.GenReqDefForUpdateHost() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.UpdateHostResponse), nil } } func (c *WafClient) UpdatePremiumHost(request *model.UpdatePremiumHostRequest) (*model.UpdatePremiumHostResponse, error) { requestDef := hwwaf.GenReqDefForUpdatePremiumHost() if resp, err := c.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { return resp.(*model.UpdatePremiumHostResponse), nil } } ================================================ FILE: pkg/core/deployer/providers/jdcloud-alb/consts.go ================================================ package jdcloudalb const ( // 资源类型:部署到指定负载均衡器。 RESOURCE_TYPE_LOADBALANCER = "loadbalancer" // 资源类型:部署到指定监听器。 RESOURCE_TYPE_LISTENER = "listener" ) ================================================ FILE: pkg/core/deployer/providers/jdcloud-alb/internal/client.go ================================================ package internal import ( "encoding/json" "errors" "github.com/jdcloud-api/jdcloud-sdk-go/core" lb "github.com/jdcloud-api/jdcloud-sdk-go/services/lb/apis" ) // This is a partial copy of https://github.com/jdcloud-api/jdcloud-sdk-go/blob/master/services/lb/client/LbClient.go // to lightweight the vendor packages in the built binary. type LbClient struct { core.JDCloudClient } func NewLbClient(credential *core.Credential) *LbClient { if credential == nil { return nil } config := core.NewConfig() config.SetEndpoint("lb.jdcloud-api.com") return &LbClient{ core.JDCloudClient{ Credential: *credential, Config: *config, ServiceName: "lb", Revision: "0.6.6", Logger: core.NewDefaultLogger(core.LogInfo), }, } } func (c *LbClient) DescribeListener(request *lb.DescribeListenerRequest) (*lb.DescribeListenerResponse, error) { if request == nil { return nil, errors.New("Request object is nil.") } resp, err := c.Send(request, c.ServiceName) if err != nil { return nil, err } jdResp := &lb.DescribeListenerResponse{} err = json.Unmarshal(resp, jdResp) if err != nil { c.Logger.Log(core.LogError, "Unmarshal json failed, resp: %s", string(resp)) return nil, err } return jdResp, err } func (c *LbClient) DescribeListeners(request *lb.DescribeListenersRequest) (*lb.DescribeListenersResponse, error) { if request == nil { return nil, errors.New("Request object is nil.") } resp, err := c.Send(request, c.ServiceName) if err != nil { return nil, err } jdResp := &lb.DescribeListenersResponse{} err = json.Unmarshal(resp, jdResp) if err != nil { c.Logger.Log(core.LogError, "Unmarshal json failed, resp: %s", string(resp)) return nil, err } return jdResp, err } func (c *LbClient) DescribeLoadBalancer(request *lb.DescribeLoadBalancerRequest) (*lb.DescribeLoadBalancerResponse, error) { if request == nil { return nil, errors.New("Request object is nil.") } resp, err := c.Send(request, c.ServiceName) if err != nil { return nil, err } jdResp := &lb.DescribeLoadBalancerResponse{} err = json.Unmarshal(resp, jdResp) if err != nil { c.Logger.Log(core.LogError, "Unmarshal json failed, resp: %s", string(resp)) return nil, err } return jdResp, err } func (c *LbClient) UpdateListener(request *lb.UpdateListenerRequest) (*lb.UpdateListenerResponse, error) { if request == nil { return nil, errors.New("Request object is nil.") } resp, err := c.Send(request, c.ServiceName) if err != nil { return nil, err } jdResp := &lb.UpdateListenerResponse{} err = json.Unmarshal(resp, jdResp) if err != nil { c.Logger.Log(core.LogError, "Unmarshal json failed, resp: %s", string(resp)) return nil, err } return jdResp, err } func (c *LbClient) UpdateListenerCertificates(request *lb.UpdateListenerCertificatesRequest) (*lb.UpdateListenerCertificatesResponse, error) { if request == nil { return nil, errors.New("Request object is nil.") } resp, err := c.Send(request, c.ServiceName) if err != nil { return nil, err } jdResp := &lb.UpdateListenerCertificatesResponse{} err = json.Unmarshal(resp, jdResp) if err != nil { c.Logger.Log(core.LogError, "Unmarshal json failed, resp: %s", string(resp)) return nil, err } return jdResp, err } ================================================ FILE: pkg/core/deployer/providers/jdcloud-alb/jdcloud_alb.go ================================================ package jdcloudalb import ( "context" "errors" "fmt" "log/slog" "strings" jdcore "github.com/jdcloud-api/jdcloud-sdk-go/core" jdcommon "github.com/jdcloud-api/jdcloud-sdk-go/services/common/models" jdlb "github.com/jdcloud-api/jdcloud-sdk-go/services/lb/apis" jdlbmodel "github.com/jdcloud-api/jdcloud-sdk-go/services/lb/models" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/jdcloud-ssl" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-alb/internal" ) type DeployerConfig struct { // 京东云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 京东云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 京东云地域 ID。 RegionId string `json:"regionId"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 负载均衡器 ID。 // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER] 时必填。 LoadbalancerId string `json:"loadbalancerId,omitempty"` // 监听器 ID。 // 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。 ListenerId string `json:"listenerId,omitempty"` // SNI 域名(支持泛域名)。 // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时选填。 Domain string `json:"domain,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.LbClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_LOADBALANCER: if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil { return nil, err } case RESOURCE_TYPE_LISTENER: if err := d.deployToListener(ctx, upres.CertId); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error { if d.config.LoadbalancerId == "" { return errors.New("config `loadbalancerId` is required") } // 查询负载均衡器详情 // REF: https://docs.jdcloud.com/cn/load-balancer/api/describeloadbalancer describeLoadBalancerReq := jdlb.NewDescribeLoadBalancerRequestWithoutParam() describeLoadBalancerReq.SetRegionId(d.config.RegionId) describeLoadBalancerReq.SetLoadBalancerId(d.config.LoadbalancerId) describeLoadBalancerResp, err := d.sdkClient.DescribeLoadBalancer(describeLoadBalancerReq) d.logger.Debug("sdk request 'lb.DescribeLoadBalancer'", slog.Any("request", describeLoadBalancerReq), slog.Any("response", describeLoadBalancerResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'lb.DescribeLoadBalancer': %w", err) } // 查询监听器列表 // REF: https://docs.jdcloud.com/cn/load-balancer/api/describelisteners listenerIds := make([]string, 0) describeListenersPageNumber := 1 describeListenersPageSize := 100 for { select { case <-ctx.Done(): return ctx.Err() default: } describeListenersReq := jdlb.NewDescribeListenersRequestWithoutParam() describeListenersReq.SetRegionId(d.config.RegionId) describeListenersReq.SetFilters([]jdcommon.Filter{{Name: "loadBalancerId", Values: []string{d.config.LoadbalancerId}}}) describeListenersReq.SetPageSize(describeListenersPageNumber) describeListenersReq.SetPageSize(describeListenersPageSize) describeListenersResp, err := d.sdkClient.DescribeListeners(describeListenersReq) d.logger.Debug("sdk request 'lb.DescribeListeners'", slog.Any("request", describeListenersReq), slog.Any("response", describeListenersResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'lb.DescribeListeners': %w", err) } for _, listener := range describeListenersResp.Result.Listeners { if strings.EqualFold(listener.Protocol, "https") || strings.EqualFold(listener.Protocol, "tls") { listenerIds = append(listenerIds, listener.ListenerId) } } if len(describeListenersResp.Result.Listeners) < describeListenersPageSize { break } describeListenersPageNumber++ } // 遍历更新监听器证书 if len(listenerIds) == 0 { d.logger.Info("no listeners to deploy") } else { d.logger.Info("found https/tls listeners to deploy", slog.Any("listenerIds", listenerIds)) var errs []error for _, listenerId := range listenerIds { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error { if d.config.ListenerId == "" { return errors.New("config `listenerId` is required") } // 更新监听器证书 if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil { return err } return nil } func (d *Deployer) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error { // 查询监听器详情 // REF: https://docs.jdcloud.com/cn/load-balancer/api/describelistener describeListenerReq := jdlb.NewDescribeListenerRequestWithoutParam() describeListenerReq.SetRegionId(d.config.RegionId) describeListenerReq.SetListenerId(cloudListenerId) describeListenerResp, err := d.sdkClient.DescribeListener(describeListenerReq) d.logger.Debug("sdk request 'lb.DescribeListener'", slog.Any("request", describeListenerReq), slog.Any("response", describeListenerResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'lb.DescribeListener': %w", err) } if d.config.Domain == "" { // 未指定 SNI,只需部署到监听器 // 修改监听器信息 // REF: https://docs.jdcloud.com/cn/load-balancer/api/updatelistener updateListenerReq := jdlb.NewUpdateListenerRequestWithoutParam() updateListenerReq.SetRegionId(d.config.RegionId) updateListenerReq.SetListenerId(cloudListenerId) updateListenerReq.SetCertificateSpecs([]jdlbmodel.CertificateSpec{{CertificateId: cloudCertId}}) updateListenerResp, err := d.sdkClient.UpdateListener(updateListenerReq) d.logger.Debug("sdk request 'lb.UpdateListener'", slog.Any("request", updateListenerReq), slog.Any("response", updateListenerResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'lb.UpdateListener': %w", err) } } else { // 指定 SNI,需部署到扩展证书 extCertSpecs := lo.Filter(describeListenerResp.Result.Listener.ExtensionCertificateSpecs, func(extCertSpec jdlbmodel.ExtensionCertificateSpec, _ int) bool { return extCertSpec.Domain == d.config.Domain }) if len(extCertSpecs) == 0 { return errors.New("could not find any extension certificates") } // 批量修改扩展证书 // REF: https://docs.jdcloud.com/cn/load-balancer/api/updatelistenercertificates updateListenerCertificatesReq := jdlb.NewUpdateListenerCertificatesRequestWithoutParam() updateListenerCertificatesReq.SetRegionId(d.config.RegionId) updateListenerCertificatesReq.SetListenerId(cloudListenerId) updateListenerCertificatesReq.SetCertificates(lo.Map(extCertSpecs, func(extCertSpec jdlbmodel.ExtensionCertificateSpec, _ int) jdlbmodel.ExtCertificateUpdateSpec { return jdlbmodel.ExtCertificateUpdateSpec{ CertificateBindId: extCertSpec.CertificateBindId, CertificateId: &cloudCertId, Domain: &extCertSpec.Domain, } })) updateListenerCertificatesResp, err := d.sdkClient.UpdateListenerCertificates(updateListenerCertificatesReq) d.logger.Debug("sdk request 'lb.UpdateListenerCertificates'", slog.Any("request", updateListenerCertificatesReq), slog.Any("response", updateListenerCertificatesResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'lb.UpdateListenerCertificates': %w", err) } } return nil } func createSDKClient(accessKeyId, accessKeySecret string) (*internal.LbClient, error) { clientCredentials := jdcore.NewCredentials(accessKeyId, accessKeySecret) client := internal.NewLbClient(clientCredentials) return client, nil } ================================================ FILE: pkg/core/deployer/providers/jdcloud-alb/jdcloud_alb_test.go ================================================ package jdcloudalb_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-alb" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fRegionId string fLoadbalancerId string fListenerId string ) func init() { argsPrefix := "JDCLOUDALB_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fRegionId, argsPrefix+"REGIONID", "", "") flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "") flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "") } /* Shell command to run this test: go test -v ./jdcloud_alb_test.go -args \ --JDCLOUDALB_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --JDCLOUDALB_INPUTKEYPATH="/path/to/your-input-key.pem" \ --JDCLOUDALB_ACCESSKEYID="your-access-key-id" \ --JDCLOUDALB_ACCESSKEYSECRET="your-secret-access-key" \ --JDCLOUDALB_REGION_ID="cn-north-1" \ --JDCLOUDALB_LOADBALANCERID="your-alb-loadbalancer-id" \ --JDCLOUDALB_LISTENERID="your-alb-listener-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy_ToLoadbalancer", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGIONID: %v", fRegionId), fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, RegionId: fRegionId, ResourceType: provider.RESOURCE_TYPE_LOADBALANCER, LoadbalancerId: fLoadbalancerId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) t.Run("Deploy_ToListener", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGIONID: %v", fRegionId), fmt.Sprintf("LISTENERID: %v", fListenerId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, RegionId: fRegionId, ResourceType: provider.RESOURCE_TYPE_LISTENER, ListenerId: fListenerId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/jdcloud-cdn/consts.go ================================================ package jdcloudcdn const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/jdcloud-cdn/internal/client.go ================================================ package internal import ( "encoding/json" "errors" "github.com/jdcloud-api/jdcloud-sdk-go/core" cdn "github.com/jdcloud-api/jdcloud-sdk-go/services/cdn/apis" ) // This is a partial copy of https://github.com/jdcloud-api/jdcloud-sdk-go/blob/master/services/cdn/client/CdnClient.go // to lightweight the vendor packages in the built binary. type CdnClient struct { core.JDCloudClient } func NewCdnClient(credential *core.Credential) *CdnClient { if credential == nil { return nil } config := core.NewConfig() config.SetEndpoint("cdn.jdcloud-api.com") return &CdnClient{ core.JDCloudClient{ Credential: *credential, Config: *config, ServiceName: "cdn", Revision: "0.10.47", Logger: core.NewDummyLogger(), }, } } func (c *CdnClient) GetDomainList(request *cdn.GetDomainListRequest) (*cdn.GetDomainListResponse, error) { if request == nil { return nil, errors.New("Request object is nil.") } resp, err := c.Send(request, c.ServiceName) if err != nil { return nil, err } jdResp := &cdn.GetDomainListResponse{} err = json.Unmarshal(resp, jdResp) if err != nil { c.Logger.Log(core.LogError, "Unmarshal json failed, resp: %s", string(resp)) return nil, err } return jdResp, err } func (c *CdnClient) QueryDomainConfig(request *cdn.QueryDomainConfigRequest) (*cdn.QueryDomainConfigResponse, error) { if request == nil { return nil, errors.New("Request object is nil.") } resp, err := c.Send(request, c.ServiceName) if err != nil { return nil, err } jdResp := &cdn.QueryDomainConfigResponse{} err = json.Unmarshal(resp, jdResp) if err != nil { c.Logger.Log(core.LogError, "Unmarshal json failed, resp: %s", string(resp)) return nil, err } return jdResp, err } func (c *CdnClient) SetHttpType(request *cdn.SetHttpTypeRequest) (*cdn.SetHttpTypeResponse, error) { if request == nil { return nil, errors.New("Request object is nil.") } resp, err := c.Send(request, c.ServiceName) if err != nil { return nil, err } jdResp := &cdn.SetHttpTypeResponse{} err = json.Unmarshal(resp, jdResp) if err != nil { c.Logger.Log(core.LogError, "Unmarshal json failed, resp: %s", string(resp)) return nil, err } return jdResp, err } ================================================ FILE: pkg/core/deployer/providers/jdcloud-cdn/jdcloud_cdn.go ================================================ package jdcloudcdn import ( "context" "errors" "fmt" "log/slog" "strings" jdcore "github.com/jdcloud-api/jdcloud-sdk-go/core" jdcdn "github.com/jdcloud-api/jdcloud-sdk-go/services/cdn/apis" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/jdcloud-ssl" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-cdn/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 京东云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 京东云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.CdnClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return xcerthostname.IsMatch(d.config.Domain, domain) }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by wildcard") } } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no cdn domains to deploy") } else { d.logger.Info("found cdn domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 查询域名列表 // REF: https://docs.jdcloud.com/cn/cdn/api/getdomainlist getDomainListPageNumber := 1 getDomainListPageSize := 50 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } getDomainListReq := jdcdn.NewGetDomainListRequestWithoutParam() getDomainListReq.SetPageNumber(getDomainListPageNumber) getDomainListReq.SetPageSize(getDomainListPageSize) getDomainListResp, err := d.sdkClient.GetDomainList(getDomainListReq) d.logger.Debug("sdk request 'cdn.GetDomainList'", slog.Any("request", getDomainListReq), slog.Any("response", getDomainListResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.GetDomainList': %w", err) } ignoredStatuses := []string{"offline"} for _, domainItem := range getDomainListResp.Result.Domains { if lo.Contains(ignoredStatuses, domainItem.Status) { continue } domains = append(domains, domainItem.Domain) } if len(getDomainListResp.Result.Domains) < getDomainListPageSize { break } getDomainListPageNumber++ } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error { // 查询域名配置信息 // REF: https://docs.jdcloud.com/cn/cdn/api/querydomainconfig queryDomainConfigReq := jdcdn.NewQueryDomainConfigRequestWithoutParam() queryDomainConfigReq.SetDomain(domain) queryDomainConfigResp, err := d.sdkClient.QueryDomainConfig(queryDomainConfigReq) d.logger.Debug("sdk request 'cdn.QueryDomainConfig'", slog.Any("request", queryDomainConfigReq), slog.Any("response", queryDomainConfigResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'cdn.QueryDomainConfig': %w", err) } // 设置通讯协议 // REF: https://docs.jdcloud.com/cn/cdn/api/sethttptype setHttpTypeReq := jdcdn.NewSetHttpTypeRequestWithoutParam() setHttpTypeReq.SetDomain(domain) setHttpTypeReq.SetHttpType("https") setHttpTypeReq.SetCertFrom("ssl") setHttpTypeReq.SetSslCertId(cloudCertId) setHttpTypeReq.SetJumpType(queryDomainConfigResp.Result.HttpsJumpType) setHttpTypeResp, err := d.sdkClient.SetHttpType(setHttpTypeReq) d.logger.Debug("sdk request 'cdn.QueryDomainConfig'", slog.Any("request", setHttpTypeReq), slog.Any("response", setHttpTypeResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'cdn.SetHttpType': %w", err) } return nil } func createSDKClient(accessKeyId, accessKeySecret string) (*internal.CdnClient, error) { clientCredentials := jdcore.NewCredentials(accessKeyId, accessKeySecret) client := internal.NewCdnClient(clientCredentials) return client, nil } ================================================ FILE: pkg/core/deployer/providers/jdcloud-cdn/jdcloud_cdn_test.go ================================================ package jdcloudcdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-cdn" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fDomain string ) func init() { argsPrefix := "JDCLOUDCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./jdcloud_cdn_test.go -args \ --JDCLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --JDCLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --JDCLOUDCDN_ACCESSKEYID="your-access-key-id" \ --JDCLOUDCDN_ACCESSKEYSECRET="your-secret-access-key" \ --JDCLOUDCDN_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/jdcloud-live/consts.go ================================================ package jdcloudlive const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/jdcloud-live/internal/client.go ================================================ package internal import ( "encoding/json" "errors" "github.com/jdcloud-api/jdcloud-sdk-go/core" live "github.com/jdcloud-api/jdcloud-sdk-go/services/live/apis" ) // This is a partial copy of https://github.com/jdcloud-api/jdcloud-sdk-go/blob/master/services/live/client/LiveClient.go // to lightweight the vendor packages in the built binary. type LiveClient struct { core.JDCloudClient } func NewLiveClient(credential *core.Credential) *LiveClient { if credential == nil { return nil } config := core.NewConfig() config.SetEndpoint("live.jdcloud-api.com") return &LiveClient{ core.JDCloudClient{ Credential: *credential, Config: *config, ServiceName: "live", Revision: "1.0.22", Logger: core.NewDummyLogger(), }, } } func (c *LiveClient) DescribeLiveDomains(request *live.DescribeLiveDomainsRequest) (*live.DescribeLiveDomainsResponse, error) { if request == nil { return nil, errors.New("Request object is nil.") } resp, err := c.Send(request, c.ServiceName) if err != nil { return nil, err } jdResp := &live.DescribeLiveDomainsResponse{} err = json.Unmarshal(resp, jdResp) if err != nil { c.Logger.Log(core.LogError, "Unmarshal json failed, resp: %s", string(resp)) return nil, err } return jdResp, err } func (c *LiveClient) SetLiveDomainCertificate(request *live.SetLiveDomainCertificateRequest) (*live.SetLiveDomainCertificateResponse, error) { if request == nil { return nil, errors.New("Request object is nil.") } resp, err := c.Send(request, c.ServiceName) if err != nil { return nil, err } jdResp := &live.SetLiveDomainCertificateResponse{} err = json.Unmarshal(resp, jdResp) if err != nil { c.Logger.Log(core.LogError, "Unmarshal json failed, resp: %s", string(resp)) return nil, err } return jdResp, err } ================================================ FILE: pkg/core/deployer/providers/jdcloud-live/jdcloud_live.go ================================================ package jdcloudlive import ( "context" "errors" "fmt" "log/slog" jdcore "github.com/jdcloud-api/jdcloud-sdk-go/core" jdlive "github.com/jdcloud-api/jdcloud-sdk-go/services/live/apis" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-live/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type DeployerConfig struct { // 京东云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 京东云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 直播播放域名(不支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.LiveClient } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret) 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) (*deployer.DeployResult, error) { // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no live domains to deploy") } else { d.logger.Info("found live domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, certPEM, privkeyPEM); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 查询域名列表 // REF: https://docs.jdcloud.com/cn/live-video/api/describelivedomains describeLiveDomainsPageNumber := 1 describeLiveDomainsPageSize := 100 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } describeLiveDomainsReq := jdlive.NewDescribeLiveDomainsRequestWithoutParam() describeLiveDomainsReq.SetPageNum(describeLiveDomainsPageNumber) describeLiveDomainsReq.SetPageSize(describeLiveDomainsPageSize) describeLiveDomainsResp, err := d.sdkClient.DescribeLiveDomains(describeLiveDomainsReq) d.logger.Debug("sdk request 'live.DescribeLiveDomainsRequest'", slog.Any("request", describeLiveDomainsReq), slog.Any("response", describeLiveDomainsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'live.DescribeLiveDomainsRequest': %w", err) } ignoredStatuses := []string{"offline", "checking", "check_failed"} for _, domainItem := range describeLiveDomainsResp.Result.DomainDetails { for _, playDomainItem := range domainItem.PlayDomains { if lo.Contains(ignoredStatuses, playDomainItem.DomainStatus) { continue } domains = append(domains, playDomainItem.PlayDomain) } } if len(describeLiveDomainsResp.Result.DomainDetails) < describeLiveDomainsPageSize { break } describeLiveDomainsPageNumber++ } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, certPEM, privkeyPEM string) error { // 设置直播证书 // REF: https://docs.jdcloud.com/cn/live-video/api/setlivedomaincertificate setLiveDomainCertificateReq := jdlive.NewSetLiveDomainCertificateRequestWithoutParam() setLiveDomainCertificateReq.SetPlayDomain(domain) setLiveDomainCertificateReq.SetCertStatus("on") setLiveDomainCertificateReq.SetCert(certPEM) setLiveDomainCertificateReq.SetKey(privkeyPEM) setLiveDomainCertificateResp, err := d.sdkClient.SetLiveDomainCertificate(setLiveDomainCertificateReq) d.logger.Debug("sdk request 'live.SetLiveDomainCertificate'", slog.Any("request", setLiveDomainCertificateReq), slog.Any("response", setLiveDomainCertificateResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'live.SetLiveDomainCertificate': %w", err) } return nil } func createSDKClient(accessKeyId, accessKeySecret string) (*internal.LiveClient, error) { clientCredentials := jdcore.NewCredentials(accessKeyId, accessKeySecret) client := internal.NewLiveClient(clientCredentials) return client, nil } ================================================ FILE: pkg/core/deployer/providers/jdcloud-live/jdcloud_live_test.go ================================================ package jdcloudlive_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-live" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fDomain string ) func init() { argsPrefix := "JDCLOUDLIVE_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./jdcloud_live_test.go -args \ --JDCLOUDLIVE_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --JDCLOUDLIVE_INPUTKEYPATH="/path/to/your-input-key.pem" \ --JDCLOUDLIVE_ACCESSKEYID="your-access-key-id" \ --JDCLOUDLIVE_ACCESSKEYSECRET="your-secret-access-key" \ --JDCLOUDLIVE_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/jdcloud-vod/consts.go ================================================ package jdcloudvod const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/jdcloud-vod/internal/client.go ================================================ package internal import ( "encoding/json" "errors" "github.com/jdcloud-api/jdcloud-sdk-go/core" vod "github.com/jdcloud-api/jdcloud-sdk-go/services/vod/apis" ) // This is a partial copy of https://github.com/jdcloud-api/jdcloud-sdk-go/blob/master/services/vod/client/VodClient.go // to lightweight the vendor packages in the built binary. type VodClient struct { core.JDCloudClient } func NewVodClient(credential *core.Credential) *VodClient { if credential == nil { return nil } config := core.NewConfig() config.SetEndpoint("vod.jdcloud-api.com") return &VodClient{ core.JDCloudClient{ Credential: *credential, Config: *config, ServiceName: "vod", Revision: "1.2.1", Logger: core.NewDummyLogger(), }, } } func (c *VodClient) ListDomains(request *vod.ListDomainsRequest) (*vod.ListDomainsResponse, error) { if request == nil { return nil, errors.New("Request object is nil.") } resp, err := c.Send(request, c.ServiceName) if err != nil { return nil, err } jdResp := &vod.ListDomainsResponse{} err = json.Unmarshal(resp, jdResp) if err != nil { c.Logger.Log(core.LogError, "Unmarshal json failed, resp: %s", string(resp)) return nil, err } return jdResp, err } func (c *VodClient) GetHttpSsl(request *vod.GetHttpSslRequest) (*vod.GetHttpSslResponse, error) { if request == nil { return nil, errors.New("Request object is nil.") } resp, err := c.Send(request, c.ServiceName) if err != nil { return nil, err } jdResp := &vod.GetHttpSslResponse{} err = json.Unmarshal(resp, jdResp) if err != nil { c.Logger.Log(core.LogError, "Unmarshal json failed, resp: %s", string(resp)) return nil, err } return jdResp, err } func (c *VodClient) SetHttpSsl(request *vod.SetHttpSslRequest) (*vod.SetHttpSslResponse, error) { if request == nil { return nil, errors.New("Request object is nil.") } resp, err := c.Send(request, c.ServiceName) if err != nil { return nil, err } jdResp := &vod.SetHttpSslResponse{} err = json.Unmarshal(resp, jdResp) if err != nil { c.Logger.Log(core.LogError, "Unmarshal json failed, resp: %s", string(resp)) return nil, err } return jdResp, err } ================================================ FILE: pkg/core/deployer/providers/jdcloud-vod/jdcloud_vod.go ================================================ package jdcloudvod import ( "context" "errors" "fmt" "log/slog" "strconv" "time" jdcore "github.com/jdcloud-api/jdcloud-sdk-go/core" jdvod "github.com/jdcloud-api/jdcloud-sdk-go/services/vod/apis" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-vod/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type DeployerConfig struct { // 京东云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 京东云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 点播加速域名(不支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.VodClient } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret) 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) (*deployer.DeployResult, error) { // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no vod domains to deploy") } else { d.logger.Info("found vod domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, certPEM, privkeyPEM); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 查询域名列表 // REF: https://docs.jdcloud.com/cn/video-on-demand/api/listdomains listDomainsPageNumber := 1 listDomainsPageSize := 100 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } listDomainsReq := jdvod.NewListDomainsRequestWithoutParam() listDomainsReq.SetPageNumber(listDomainsPageNumber) listDomainsReq.SetPageSize(listDomainsPageSize) listDomainsResp, err := d.sdkClient.ListDomains(listDomainsReq) d.logger.Debug("sdk request 'vod.ListDomains'", slog.Any("request", listDomainsReq), slog.Any("response", listDomainsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'vod.ListDomains': %w", err) } ignoredStatuses := []string{"init", "stopped"} for _, domainItem := range listDomainsResp.Result.Content { if lo.Contains(ignoredStatuses, domainItem.Status) { continue } domains = append(domains, domainItem.Name) } if len(listDomainsResp.Result.Content) < listDomainsPageSize { break } listDomainsPageNumber++ } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, certPEM, privkeyPEM string) error { // 获取域名 ID domainId, err := d.findDomainIdByDomain(ctx, domain) if err != nil { return err } // 查询域名 SSL 配置 // REF: https://docs.jdcloud.com/cn/video-on-demand/api/gethttpssl getHttpSslReq := jdvod.NewGetHttpSslRequestWithoutParam() getHttpSslReq.SetDomainId(domainId) getHttpSslResp, err := d.sdkClient.GetHttpSsl(getHttpSslReq) d.logger.Debug("sdk request 'vod.GetHttpSsl'", slog.Any("request", getHttpSslReq), slog.Any("response", getHttpSslResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'vod.GetHttpSsl': %w", err) } // 设置域名 SSL 配置 // REF: https://docs.jdcloud.com/cn/video-on-demand/api/sethttpssl setHttpSslReq := jdvod.NewSetHttpSslRequestWithoutParam() setHttpSslReq.SetDomainId(domainId) setHttpSslReq.SetTitle(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())) setHttpSslReq.SetSslCert(certPEM) setHttpSslReq.SetSslKey(privkeyPEM) setHttpSslReq.SetSource("default") setHttpSslReq.SetJumpType(getHttpSslResp.Result.JumpType) setHttpSslReq.SetEnabled(true) setHttpSslResp, err := d.sdkClient.SetHttpSsl(setHttpSslReq) d.logger.Debug("sdk request 'vod.SetHttpSsl'", slog.Any("request", setHttpSslReq), slog.Any("response", setHttpSslResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'vod.SetHttpSsl': %w", err) } return nil } func (d *Deployer) findDomainIdByDomain(ctx context.Context, domain string) (int, error) { // 查询域名列表 // REF: https://docs.jdcloud.com/cn/video-on-demand/api/listdomains listDomainsPageNumber := 1 listDomainsPageSize := 100 for { select { case <-ctx.Done(): return 0, ctx.Err() default: } listDomainsReq := jdvod.NewListDomainsRequestWithoutParam() listDomainsReq.SetPageNumber(listDomainsPageNumber) listDomainsReq.SetPageSize(listDomainsPageSize) listDomainsResp, err := d.sdkClient.ListDomains(listDomainsReq) d.logger.Debug("sdk request 'vod.ListDomains'", slog.Any("request", listDomainsReq), slog.Any("response", listDomainsResp)) if err != nil { return 0, fmt.Errorf("failed to execute sdk request 'vod.ListDomains': %w", err) } for _, domainItem := range listDomainsResp.Result.Content { if domainItem.Name == domain { domainId, _ := strconv.Atoi(domainItem.Id) return domainId, nil } } if len(listDomainsResp.Result.Content) < listDomainsPageSize { break } listDomainsPageNumber++ } return 0, fmt.Errorf("could not find domain '%s'", domain) } func createSDKClient(accessKeyId, accessKeySecret string) (*internal.VodClient, error) { clientCredentials := jdcore.NewCredentials(accessKeyId, accessKeySecret) client := internal.NewVodClient(clientCredentials) return client, nil } ================================================ FILE: pkg/core/deployer/providers/jdcloud-vod/jdcloud_vod_test.go ================================================ package jdcloudvod_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/jdcloud-vod" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fDomain string ) func init() { argsPrefix := "JDCLOUDVOD_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./jdcloud_vod_test.go -args \ --JDCLOUDVOD_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --JDCLOUDVOD_INPUTKEYPATH="/path/to/your-input-key.pem" \ --JDCLOUDVOD_ACCESSKEYID="your-access-key-id" \ --JDCLOUDVOD_ACCESSKEYSECRET="your-secret-access-key" \ --JDCLOUDVOD_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/k8s-secret/k8s_secret.go ================================================ package k8ssecret import ( "context" "errors" "fmt" "log/slog" "strings" k8score "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" k8smeta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "github.com/certimate-go/certimate/pkg/core/deployer" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type DeployerConfig struct { // kubeconfig 文件内容。 KubeConfig string `json:"kubeConfig,omitempty"` // Kubernetes 命名空间。 Namespace string `json:"namespace,omitempty"` // Kubernetes Secret 名称。 SecretName string `json:"secretName"` // Kubernetes Secret 类型。 SecretType string `json:"secretType"` // Kubernetes Secret 中用于存放证书的 Key。 SecretDataKeyForCrt string `json:"secretDataKeyForCrt,omitempty"` // Kubernetes Secret 中用于存放私钥的 Key。 SecretDataKeyForKey string `json:"secretDataKeyForKey,omitempty"` // Kubernetes Secret 注解。 SecretAnnotations map[string]string `json:"secretAnnotations,omitempty"` // Kubernetes Secret 标签。 SecretLabels map[string]string `json:"secretLabels,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } return &Deployer{ logger: slog.Default(), config: config, }, 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) (*deployer.DeployResult, error) { if d.config.Namespace == "" { return nil, errors.New("config `namespace` is required") } if d.config.SecretName == "" { return nil, errors.New("config `secretName` is required") } if d.config.SecretType == "" { return nil, errors.New("config `secretType` is required") } if d.config.SecretDataKeyForCrt == "" { return nil, errors.New("config `secretDataKeyForCrt` is required") } if d.config.SecretDataKeyForKey == "" { return nil, errors.New("config `secretDataKeyForKey` is required") } certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } // 连接 client, err := createK8sClient(d.config.KubeConfig) if err != nil { return nil, fmt.Errorf("failed to create kubernetes client: %w", err) } var secretPayload *k8score.Secret secretAnnotations := map[string]string{ "certimate/common-name": certX509.Subject.CommonName, "certimate/subject-sn": certX509.Subject.SerialNumber, "certimate/subject-alt-names": strings.Join(certX509.DNSNames, ","), "certimate/issuer-sn": certX509.Issuer.SerialNumber, "certimate/issuer-org": strings.Join(certX509.Issuer.Organization, ","), } secretLabels := map[string]string{} if d.config.SecretAnnotations != nil { for k, v := range d.config.SecretAnnotations { secretAnnotations[k] = v } } if d.config.SecretLabels != nil { for k, v := range d.config.SecretLabels { secretLabels[k] = v } } // 获取 Secret 实例,如果不存在则创建 secretPayload, err = client.CoreV1().Secrets(d.config.Namespace).Get(ctx, d.config.SecretName, k8smeta.GetOptions{}) if err != nil { if !k8serrors.IsNotFound(err) { return nil, fmt.Errorf("failed to get kubernetes secret: %w", err) } secretPayload = &k8score.Secret{ TypeMeta: k8smeta.TypeMeta{ Kind: "Secret", APIVersion: "v1", }, ObjectMeta: k8smeta.ObjectMeta{ Name: d.config.SecretName, Annotations: secretAnnotations, Labels: secretLabels, }, Type: k8score.SecretType(d.config.SecretType), } secretPayload.Data = make(map[string][]byte) secretPayload.Data[d.config.SecretDataKeyForCrt] = []byte(certPEM) secretPayload.Data[d.config.SecretDataKeyForKey] = []byte(privkeyPEM) secretPayload, err = client.CoreV1().Secrets(d.config.Namespace).Create(ctx, secretPayload, k8smeta.CreateOptions{}) d.logger.Debug("kubernetes operate 'Secrets.Create'", slog.String("namespace", d.config.Namespace), slog.Any("secret", secretPayload)) if err != nil { return nil, fmt.Errorf("failed to create kubernetes secret: %w", err) } else { return &deployer.DeployResult{}, nil } } // 更新 Secret 实例 secretPayload.Type = k8score.SecretType(d.config.SecretType) if secretPayload.ObjectMeta.Annotations == nil { secretPayload.ObjectMeta.Annotations = secretAnnotations } else { for k, v := range secretAnnotations { secretPayload.ObjectMeta.Annotations[k] = v } } if secretPayload.ObjectMeta.Labels == nil { secretPayload.ObjectMeta.Labels = secretLabels } else { for k, v := range secretLabels { secretPayload.ObjectMeta.Labels[k] = v } } if secretPayload.Data == nil { secretPayload.Data = make(map[string][]byte) } secretPayload.Data[d.config.SecretDataKeyForCrt] = []byte(certPEM) secretPayload.Data[d.config.SecretDataKeyForKey] = []byte(privkeyPEM) secretPayload, err = client.CoreV1().Secrets(d.config.Namespace).Update(ctx, secretPayload, k8smeta.UpdateOptions{}) d.logger.Debug("kubernetes operate 'Secrets.Update'", slog.String("namespace", d.config.Namespace), slog.Any("secret", secretPayload)) if err != nil { return nil, fmt.Errorf("failed to update kubernetes secret: %w", err) } return &deployer.DeployResult{}, nil } func createK8sClient(kubeConfig string) (*kubernetes.Clientset, error) { var config *rest.Config var err error if kubeConfig == "" { config, err = rest.InClusterConfig() } else { kubeConfig, err := clientcmd.NewClientConfigFromBytes([]byte(kubeConfig)) if err != nil { return nil, err } config, err = kubeConfig.ClientConfig() } if err != nil { return nil, err } client, err := kubernetes.NewForConfig(config) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/k8s-secret/k8s_secret_test.go ================================================ package k8ssecret_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/k8s-secret" ) var ( fInputCertPath string fInputKeyPath string fNamespace string fSecretName string fSecretDataKeyForCrt string fSecretDataKeyForKey string ) func init() { argsPrefix := "K8SSECRET_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fNamespace, argsPrefix+"NAMESPACE", "default", "") flag.StringVar(&fSecretName, argsPrefix+"SECRETNAME", "", "") flag.StringVar(&fSecretDataKeyForCrt, argsPrefix+"SECRETDATAKEYFORCRT", "tls.crt", "") flag.StringVar(&fSecretDataKeyForKey, argsPrefix+"SECRETDATAKEYFORKEY", "tls.key", "") } /* Shell command to run this test: go test -v ./k8s_secret_test.go -args \ --K8SSECRET_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --K8SSECRET_INPUTKEYPATH="/path/to/your-input-key.pem" \ --K8SSECRET_NAMESPACE="default" \ --K8SSECRET_SECRETNAME="secret" \ --K8SSECRET_SECRETDATAKEYFORCRT="tls.crt" \ --K8SSECRET_SECRETDATAKEYFORKEY="tls.key" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("NAMESPACE: %v", fNamespace), fmt.Sprintf("SECRETNAME: %v", fSecretName), fmt.Sprintf("SECRETDATAKEYFORCRT: %v", fSecretDataKeyForCrt), fmt.Sprintf("SECRETDATAKEYFORKEY: %v", fSecretDataKeyForKey), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ Namespace: fNamespace, SecretName: fSecretName, SecretDataKeyForCrt: fSecretDataKeyForCrt, SecretDataKeyForKey: fSecretDataKeyForKey, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/kong/consts.go ================================================ package kong const ( // 资源类型:替换指定证书。 RESOURCE_TYPE_CERTIFICATE = "certificate" ) ================================================ FILE: pkg/core/deployer/providers/kong/kong.go ================================================ package kong import ( "context" "crypto/tls" "errors" "fmt" "log/slog" "net/http" "github.com/kong/go-kong/kong" "github.com/certimate-go/certimate/pkg/core/deployer" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xhttp "github.com/certimate-go/certimate/pkg/utils/http" ) type DeployerConfig struct { // Kong 服务地址。 ServerUrl string `json:"serverUrl"` // Kong Admin API Token。 ApiToken string `json:"apiToken,omitempty"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 工作空间。 // 选填。 Workspace string `json:"workspace,omitempty"` // 证书 ID。 // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。 CertificateId string `json:"certificateId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *kong.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ServerUrl, config.Workspace, config.ApiToken, config.AllowInsecureConnections) 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) (*deployer.DeployResult, error) { // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_CERTIFICATE: if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.CertificateId == "" { return errors.New("config `certificateId` is required") } // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return err } // 更新证书 // REF: https://developer.konghq.com/api/gateway/admin-ee/3.10/#/operations/upsert-certificate // REF: https://developer.konghq.com/api/gateway/admin-ee/3.10/#/operations/upsert-certificate-in-workspace updateCertificateReq := &kong.Certificate{ ID: kong.String(d.config.CertificateId), Cert: kong.String(certPEM), Key: kong.String(privkeyPEM), SNIs: kong.StringSlice(certX509.DNSNames...), } updateCertificateResp, err := d.sdkClient.Certificates.Update(ctx, updateCertificateReq) d.logger.Debug("sdk request 'kong.UpdateCertificate'", slog.String("sslId", d.config.CertificateId), slog.Any("request", updateCertificateReq), slog.Any("response", updateCertificateResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'kong.UpdateCertificate': %w", err) } return nil } func createSDKClient(serverUrl, workspace, apiToken string, skipTlsVerify bool) (*kong.Client, error) { httpClient := &http.Client{ Transport: xhttp.NewDefaultTransport(), Timeout: http.DefaultClient.Timeout, } if skipTlsVerify { transport := xhttp.NewDefaultTransport() transport.DisableKeepAlives = true transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} httpClient.Transport = transport } else { httpClient.Transport = http.DefaultTransport } httpHeaders := http.Header{} if apiToken != "" { httpHeaders.Set("Kong-Admin-Token", apiToken) } client, err := kong.NewClient(kong.String(serverUrl), kong.HTTPClientWithHeaders(httpClient, httpHeaders)) if err != nil { return nil, err } if workspace != "" { client.SetWorkspace(workspace) } return client, nil } ================================================ FILE: pkg/core/deployer/providers/kong/kong_test.go ================================================ package kong_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/kong" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fApiToken string fCertificateId string ) func init() { argsPrefix := "KONG_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "") flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "") } /* Shell command to run this test: go test -v ./kong_test.go -args \ --KONG_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --KONG_INPUTKEYPATH="/path/to/your-input-key.pem" \ --KONG_SERVERURL="http://127.0.0.1:9080" \ --KONG_APITOKEN="your-admin-token" \ --KONG_CERTIFICATEID="your-certificate-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("APITOKEN: %v", fApiToken), fmt.Sprintf("CERTIFICATEID: %v", fCertificateId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ServerUrl: fServerUrl, ApiToken: fApiToken, AllowInsecureConnections: true, ResourceType: provider.RESOURCE_TYPE_CERTIFICATE, CertificateId: fCertificateId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/ksyun-cdn/consts.go ================================================ package ksyuncdn const ( // 资源类型:替换指定域名的证书。 RESOURCE_TYPE_DOMAIN = "domain" // 资源类型:替换指定证书。 RESOURCE_TYPE_CERTIFICATE = "certificate" ) const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/ksyun-cdn/ksyun_cdn.go ================================================ package ksyuncdn import ( "context" "errors" "fmt" "log/slog" "strings" "time" "github.com/KscSDK/ksc-sdk-go/ksc" ksccdnv1 "github.com/KscSDK/ksc-sdk-go/service/cdnv1" "github.com/go-viper/mapstructure/v2" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/deployer" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 金山云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 金山云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` // 证书 ID。 // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。 CertificateId string `json:"certificateId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *ksccdnv1.Cdnv1 } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey) 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) (*deployer.DeployResult, error) { // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_DOMAIN: if err := d.deployToDomain(ctx, certPEM, privkeyPEM); err != nil { return nil, err } case RESOURCE_TYPE_CERTIFICATE: if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToDomain(ctx context.Context, certPEM, privkeyPEM string) error { // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getAllDomains(ctx) if err != nil { return err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return xcerthostname.IsMatch(d.config.Domain, domain) }) if len(domains) == 0 { return errors.New("could not find any domains matched by wildcard") } } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return errors.New("could not find any domains matched by certificate") } } default: return fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no cdn domains to deploy") } else { d.logger.Info("found cdn domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, certPEM, privkeyPEM); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.CertificateId == "" { return errors.New("config `certificateId` is required") } // 更新证书 // https://docs.ksyun.com/documents/259 setCertificateInput := map[string]any{ "CertificateId": d.config.CertificateId, "CertificateName": fmt.Sprintf("certimate_%d", time.Now().UnixMilli()), "ServerCertificate": certPEM, "PrivateKey": privkeyPEM, } setCertificateReq, setCertificateOutput := d.sdkClient.SetCertificatePostRequest(&setCertificateInput) setCertificateErr := setCertificateReq.Send() d.logger.Debug("sdk request 'cdn.SetCertificate'", slog.Any("request", setCertificateInput), slog.Any("response", setCertificateOutput)) if setCertificateErr != nil { return fmt.Errorf("failed to execute sdk request 'cdn.SetCertificate': %w", setCertificateErr) } return nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 查询域名列表 // https://docs.ksyun.com/documents/198 getCdnDomainsPageNumber := 1 getCdnDomainsPageSize := 100 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } getCdnDomainsInput := map[string]any{ "PageNumber": getCdnDomainsPageNumber, "PageSize": getCdnDomainsPageSize, } getCdnDomainsReq, getCdnDomainsOutput := d.sdkClient.GetCdnDomainsPostRequest(&getCdnDomainsInput) getCdnDomainsErr := getCdnDomainsReq.Send() d.logger.Debug("sdk request 'cdn.GetCdnDomains'", slog.Any("request", getCdnDomainsInput), slog.Any("response", getCdnDomainsOutput)) if getCdnDomainsErr != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.GetCdnDomains': %w", getCdnDomainsErr) } type GetCdnDomainsResponse struct { PageNumber int32 `json:"PageNumber"` PageSize int32 `json:"PageSize"` TotalCount int32 `json:"TotalCount"` Domains []*struct { DomainId string `json:"DomainId"` DomainName string `json:"DomainName"` Cname string `json:"Cname"` CdnType string `json:"CdnType"` CreatedTime string `json:"CreatedTime"` ModifiedTime string `json:"ModifiedTime"` Region string `json:"Region"` } `json:"Domains"` } var getCdnDomainsResp *GetCdnDomainsResponse mapstructure.Decode(getCdnDomainsOutput, &getCdnDomainsResp) if getCdnDomainsResp == nil { break } for _, domainItem := range getCdnDomainsResp.Domains { domains = append(domains, domainItem.DomainName) } if len(getCdnDomainsResp.Domains) < getCdnDomainsPageSize { break } getCdnDomainsPageNumber++ } return domains, nil } func (d *Deployer) findDomainIdByDomain(ctx context.Context, domain string) (string, error) { // 查询域名列表 // https://docs.ksyun.com/documents/198 getCdnDomainsPageNumber := 1 getCdnDomainsPageSize := 100 for { select { case <-ctx.Done(): return "", ctx.Err() default: } getCdnDomainsInput := map[string]any{ "PageNumber": getCdnDomainsPageNumber, "PageSize": getCdnDomainsPageSize, "DomainName": domain, "FuzzyMatch": "off", } getCdnDomainsReq, getCdnDomainsOutput := d.sdkClient.GetCdnDomainsPostRequest(&getCdnDomainsInput) getCdnDomainsErr := getCdnDomainsReq.Send() d.logger.Debug("sdk request 'cdn.GetCdnDomains'", slog.Any("request", getCdnDomainsInput), slog.Any("response", getCdnDomainsOutput)) if getCdnDomainsErr != nil { return "", fmt.Errorf("failed to execute sdk request 'cdn.GetCdnDomains': %w", getCdnDomainsErr) } type GetCdnDomainsResponse struct { PageNumber int32 `json:"PageNumber"` PageSize int32 `json:"PageSize"` TotalCount int32 `json:"TotalCount"` Domains []*struct { DomainId string `json:"DomainId"` DomainName string `json:"DomainName"` Cname string `json:"Cname"` CdnType string `json:"CdnType"` CreatedTime string `json:"CreatedTime"` ModifiedTime string `json:"ModifiedTime"` Region string `json:"Region"` } `json:"Domains"` } var getCdnDomainsResp *GetCdnDomainsResponse mapstructure.Decode(getCdnDomainsOutput, &getCdnDomainsResp) if getCdnDomainsResp == nil { break } for _, domainItem := range getCdnDomainsResp.Domains { if strings.EqualFold(domainItem.DomainName, domain) { return domainItem.DomainId, nil } } if len(getCdnDomainsResp.Domains) < getCdnDomainsPageSize { break } getCdnDomainsPageNumber++ } return "", fmt.Errorf("could not find domain '%s'", domain) } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, certPEM, privkeyPEM string) error { // 获取域名 ID domainId, err := d.findDomainIdByDomain(ctx, domain) if err != nil { return err } // 为加速域名配置证书接口 // https://docs.ksyun.com/documents/261 configCertificateInput := map[string]any{ "Enable": "on", "DomainIds": domainId, "CertificateName": fmt.Sprintf("certimate_%d", time.Now().UnixMilli()), "ServerCertificate": certPEM, "PrivateKey": privkeyPEM, } configCertificateReq, configCertificateOutput := d.sdkClient.ConfigCertificatePostRequest(&configCertificateInput) configCertificateErr := configCertificateReq.Send() d.logger.Debug("sdk request 'cdn.ConfigCertificate'", slog.Any("request", configCertificateInput), slog.Any("response", configCertificateOutput)) if configCertificateErr != nil { return fmt.Errorf("failed to execute sdk request 'cdn.ConfigCertificate': %w", configCertificateErr) } return nil } func createSDKClient(accessKeyId, secretAccessKey string) (*ksccdnv1.Cdnv1, error) { region := "cn-beijing-6" client := ksccdnv1.SdkNew(ksc.NewClient(accessKeyId, secretAccessKey), &ksc.Config{Region: ®ion}) return client, nil } ================================================ FILE: pkg/core/deployer/providers/ksyun-cdn/ksyun_cdn_test.go ================================================ package ksyuncdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/ksyun-cdn" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fSecretAccessKey string fDomain string fCertificateId string ) func init() { argsPrefix := "KSYUNCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "") } /* Shell command to run this test: go test -v ./ksyun_cdn_test.go -args \ --KSYUNCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --KSYUNCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --KSYUNCDN_ACCESSKEYID="your-access-key-id" \ --KSYUNCDN_SECRETACCESSKEY="your-secret-access-key" \ --KSYUNCDN_DOMAIN="example.com" \ --KSYUNCDN_CERTIFICATEID="your-certificate-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), fmt.Sprintf("DOMAIN: %v", fDomain), fmt.Sprintf("CERTIFICATEID: %v", fCertificateId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, CertificateId: fCertificateId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/lecdn/consts.go ================================================ package lecdn const ( // 资源类型:替换指定证书。 RESOURCE_TYPE_CERTIFICATE = "certificate" ) ================================================ FILE: pkg/core/deployer/providers/lecdn/lecdn.go ================================================ package lecdn import ( "context" "crypto/tls" "errors" "fmt" "log/slog" "time" "github.com/certimate-go/certimate/pkg/core/deployer" leclientsdkv3 "github.com/certimate-go/certimate/pkg/sdk3rd/lecdn/v3/client" lemastersdkv3 "github.com/certimate-go/certimate/pkg/sdk3rd/lecdn/v3/master" ) type DeployerConfig struct { // LeCDN 服务地址。 ServerUrl string `json:"serverUrl"` // LeCDN 版本。 // 可取值 "v3"。 ApiVersion string `json:"apiVersion"` // LeCDN 用户角色。 // 可取值 "client"、"master"。 ApiRole string `json:"apiRole"` // LeCDN 用户名。 Username string `json:"username"` // LeCDN 用户密码。 Password string `json:"password"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 证书 ID。 // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。 CertificateId int64 `json:"certificateId,omitempty"` // 客户 ID。 // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时选填。 ClientId int64 `json:"clientId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient any } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ServerUrl, config.ApiVersion, config.ApiRole, config.Username, config.Password, config.AllowInsecureConnections) 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) (*deployer.DeployResult, error) { // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_CERTIFICATE: if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.CertificateId == 0 { return errors.New("config `certificateId` is required") } // 修改证书 // REF: https://wdk0pwf8ul.feishu.cn/wiki/YE1XwCRIHiLYeKkPupgcXrlgnDd switch sdkClient := d.sdkClient.(type) { case *leclientsdkv3.Client: { updateSSLCertReq := &leclientsdkv3.UpdateCertificateRequest{ Name: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()), Description: "upload from certimate", Type: "upload", SSLPEM: certPEM, SSLKey: privkeyPEM, AutoRenewal: false, } updateSSLCertResp, err := sdkClient.UpdateCertificateWithContext(ctx, d.config.CertificateId, updateSSLCertReq) d.logger.Debug("sdk request 'lecdn.UpdateCertificate'", slog.Int64("certId", d.config.CertificateId), slog.Any("request", updateSSLCertReq), slog.Any("response", updateSSLCertResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'lecdn.UpdateCertificate': %w", err) } } case *lemastersdkv3.Client: { updateSSLCertReq := &lemastersdkv3.UpdateCertificateRequest{ ClientId: d.config.ClientId, Name: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()), Description: "upload from certimate", Type: "upload", SSLPEM: certPEM, SSLKey: privkeyPEM, AutoRenewal: false, } updateSSLCertResp, err := sdkClient.UpdateCertificateWithContext(ctx, d.config.CertificateId, updateSSLCertReq) d.logger.Debug("sdk request 'lecdn.UpdateCertificate'", slog.Int64("certId", d.config.CertificateId), slog.Any("request", updateSSLCertReq), slog.Any("response", updateSSLCertResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'lecdn.UpdateCertificate': %w", err) } } default: panic("unreachable") } return nil } const ( sdkVersionV3 = "v3" sdkRoleClient = "client" sdkRoleMaster = "master" ) func createSDKClient(serverUrl, apiVersion, apiRole, username, password string, skipTlsVerify bool) (any, error) { if apiVersion == sdkVersionV3 && apiRole == sdkRoleClient { // v3 版客户端 client, err := leclientsdkv3.NewClient(serverUrl, username, password) if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } else if apiVersion == sdkVersionV3 && apiRole == sdkRoleMaster { // v3 版主控端 client, err := lemastersdkv3.NewClient(serverUrl, username, password) if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } return nil, errors.New("lecdn: invalid api version or user role") } ================================================ FILE: pkg/core/deployer/providers/lecdn/lecdn_test.go ================================================ package lecdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/lecdn" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fApiVersion string fUsername string fPassword string fCertificateId int64 ) func init() { argsPrefix := "LECDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fApiVersion, argsPrefix+"APIVERSION", "v3", "") flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "") flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "") flag.Int64Var(&fCertificateId, argsPrefix+"CERTIFICATEID", 0, "") } /* Shell command to run this test: go test -v ./lecdn_test.go -args \ --LECDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --LECDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --LECDN_SERVERURL="http://127.0.0.1:5090" \ --LECDN_USERNAME="your-username" \ --LECDN_PASSWORD="your-password" \ --LECDN_CERTIFICATEID="your-certificate-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy_ToCertificate", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("APIVERSION: %v", fApiVersion), fmt.Sprintf("USERNAME: %v", fUsername), fmt.Sprintf("PASSWORD: %v", fPassword), fmt.Sprintf("CERTIFICATEID: %v", fCertificateId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ServerUrl: fServerUrl, ApiVersion: fApiVersion, ApiRole: "user", Username: fUsername, Password: fPassword, AllowInsecureConnections: true, ResourceType: provider.RESOURCE_TYPE_CERTIFICATE, CertificateId: fCertificateId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/local/consts.go ================================================ package local import ( "github.com/certimate-go/certimate/internal/domain" ) const ( SHELL_ENV_SH = "sh" SHELL_ENV_CMD = "cmd" SHELL_ENV_POWERSHELL = "powershell" ) const ( OUTPUT_FORMAT_PEM = string(domain.CertificateFormatTypePEM) OUTPUT_FORMAT_PFX = string(domain.CertificateFormatTypePFX) OUTPUT_FORMAT_JKS = string(domain.CertificateFormatTypeJKS) ) ================================================ FILE: pkg/core/deployer/providers/local/local.go ================================================ package local import ( "bytes" "context" "errors" "fmt" "log/slog" "os/exec" "runtime" "strings" "github.com/certimate-go/certimate/pkg/core/deployer" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xfile "github.com/certimate-go/certimate/pkg/utils/file" ) type DeployerConfig struct { // Shell 执行环境。 // 零值时根据操作系统决定。 ShellEnv string `json:"shellEnv,omitempty"` // 前置命令。 PreCommand string `json:"preCommand,omitempty"` // 后置命令。 PostCommand string `json:"postCommand,omitempty"` // 输出证书格式。 OutputFormat string `json:"outputFormat,omitempty"` // 输出证书文件路径。 OutputCertPath string `json:"outputCertPath,omitempty"` // 输出服务器证书文件路径。 // 选填。 OutputServerCertPath string `json:"outputServerCertPath,omitempty"` // 输出中间证书文件路径。 // 选填。 OutputIntermediaCertPath string `json:"outputIntermediaCertPath,omitempty"` // 输出私钥文件路径。 OutputKeyPath string `json:"outputKeyPath,omitempty"` // PFX 导出密码。 // 证书格式为 PFX 时必填。 PfxPassword string `json:"pfxPassword,omitempty"` // JKS 别名。 // 证书格式为 JKS 时必填。 JksAlias string `json:"jksAlias,omitempty"` // JKS 密钥密码。 // 证书格式为 JKS 时必填。 JksKeypass string `json:"jksKeypass,omitempty"` // JKS 存储密码。 // 证书格式为 JKS 时必填。 JksStorepass string `json:"jksStorepass,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } return &Deployer{ config: config, logger: slog.Default(), }, 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) (*deployer.DeployResult, error) { // 提取服务器证书和中间证书 serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM) if err != nil { return nil, fmt.Errorf("failed to extract certs: %w", err) } // 执行前置命令 if d.config.PreCommand != "" { command := d.config.PreCommand command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}", d.config.OutputCertPath) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_SERVER_PATH}", d.config.OutputServerCertPath) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_INTERMEDIA_PATH}", d.config.OutputIntermediaCertPath) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_PRIVATEKEY_PATH}", d.config.OutputKeyPath) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_PFX_PASSWORD}", d.config.PfxPassword) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_ALIAS}", d.config.JksAlias) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_KEYPASS}", d.config.JksKeypass) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_STOREPASS}", d.config.JksStorepass) stdout, stderr, err := execCommand(d.config.ShellEnv, command) d.logger.Debug("run pre-command", slog.String("stdout", stdout), slog.String("stderr", stderr)) if err != nil { return nil, fmt.Errorf("failed to execute pre-command (stdout: %s, stderr: %s): %w ", stdout, stderr, err) } } // 写入证书和私钥文件 switch d.config.OutputFormat { case OUTPUT_FORMAT_PEM: { if err := xfile.WriteString(d.config.OutputCertPath, certPEM); err != nil { return nil, fmt.Errorf("failed to save certificate file: %w", err) } d.logger.Info("ssl certificate file saved", slog.String("path", d.config.OutputCertPath)) if d.config.OutputServerCertPath != "" { if err := xfile.WriteString(d.config.OutputServerCertPath, serverCertPEM); err != nil { return nil, fmt.Errorf("failed to save server certificate file: %w", err) } d.logger.Info("ssl server certificate file saved", slog.String("path", d.config.OutputServerCertPath)) } if d.config.OutputIntermediaCertPath != "" { if err := xfile.WriteString(d.config.OutputIntermediaCertPath, intermediaCertPEM); err != nil { return nil, fmt.Errorf("failed to save intermedia certificate file: %w", err) } d.logger.Info("ssl intermedia certificate file saved", slog.String("path", d.config.OutputIntermediaCertPath)) } if err := xfile.WriteString(d.config.OutputKeyPath, privkeyPEM); err != nil { return nil, fmt.Errorf("failed to save private key file: %w", err) } d.logger.Info("ssl private key file saved", slog.String("path", d.config.OutputKeyPath)) } case OUTPUT_FORMAT_PFX: { pfxData, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, d.config.PfxPassword) if err != nil { return nil, fmt.Errorf("failed to transform certificate to PFX: %w", err) } d.logger.Info("ssl certificate transformed to pfx") if err := xfile.Write(d.config.OutputCertPath, pfxData); err != nil { return nil, fmt.Errorf("failed to save certificate file: %w", err) } d.logger.Info("ssl certificate file saved", slog.String("path", d.config.OutputCertPath)) } case OUTPUT_FORMAT_JKS: { jksData, err := xcert.TransformCertificateFromPEMToJKS(certPEM, privkeyPEM, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass) if err != nil { return nil, fmt.Errorf("failed to transform certificate to JKS: %w", err) } d.logger.Info("ssl certificate transformed to jks") if err := xfile.Write(d.config.OutputCertPath, jksData); err != nil { return nil, fmt.Errorf("failed to save certificate file: %w", err) } d.logger.Info("ssl certificate file saved", slog.String("path", d.config.OutputCertPath)) } default: return nil, fmt.Errorf("unsupported output format '%s'", d.config.OutputFormat) } // 执行后置命令 if d.config.PostCommand != "" { command := d.config.PostCommand command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}", d.config.OutputCertPath) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_SERVER_PATH}", d.config.OutputServerCertPath) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_INTERMEDIA_PATH}", d.config.OutputIntermediaCertPath) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_PRIVATEKEY_PATH}", d.config.OutputKeyPath) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_PFX_PASSWORD}", d.config.PfxPassword) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_ALIAS}", d.config.JksAlias) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_KEYPASS}", d.config.JksKeypass) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_STOREPASS}", d.config.JksStorepass) stdout, stderr, err := execCommand(d.config.ShellEnv, command) d.logger.Debug("run post-command", slog.String("stdout", stdout), slog.String("stderr", stderr)) if err != nil { return nil, fmt.Errorf("failed to execute post-command (stdout: %s, stderr: %s): %w ", stdout, stderr, err) } } return &deployer.DeployResult{}, nil } func execCommand(shellEnv string, command string) (string, string, error) { var cmd *exec.Cmd switch shellEnv { case "": if runtime.GOOS == "windows" { cmd = exec.Command("cmd", "/C", command) } else { cmd = exec.Command("sh", "-c", command) } case SHELL_ENV_SH: cmd = exec.Command("sh", "-c", command) case SHELL_ENV_CMD: cmd = exec.Command("cmd", "/C", command) case SHELL_ENV_POWERSHELL: cmd = exec.Command("powershell", "-Command", command) default: return "", "", fmt.Errorf("unsupported shell env '%s'", shellEnv) } stdoutBuf := bytes.NewBuffer(nil) cmd.Stdout = stdoutBuf stderrBuf := bytes.NewBuffer(nil) cmd.Stderr = stderrBuf err := cmd.Run() if err != nil { return stdoutBuf.String(), stderrBuf.String(), fmt.Errorf("failed to execute command: %w", err) } return stdoutBuf.String(), stderrBuf.String(), nil } ================================================ FILE: pkg/core/deployer/providers/local/local_test.go ================================================ package local_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/local" ) var ( fInputCertPath string fInputKeyPath string fOutputCertPath string fOutputKeyPath string fPfxPassword string fJksAlias string fJksKeypass string fJksStorepass string fShellEnv string fPreCommand string fPostCommand string ) func init() { argsPrefix := "LOCAL_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fOutputCertPath, argsPrefix+"OUTPUTCERTPATH", "", "") flag.StringVar(&fOutputKeyPath, argsPrefix+"OUTPUTKEYPATH", "", "") flag.StringVar(&fPfxPassword, argsPrefix+"PFXPASSWORD", "", "") flag.StringVar(&fJksAlias, argsPrefix+"JKSALIAS", "", "") flag.StringVar(&fJksKeypass, argsPrefix+"JKSKEYPASS", "", "") flag.StringVar(&fJksStorepass, argsPrefix+"JKSSTOREPASS", "", "") flag.StringVar(&fShellEnv, argsPrefix+"SHELLENV", "", "") flag.StringVar(&fPreCommand, argsPrefix+"PRECOMMAND", "", "") flag.StringVar(&fPostCommand, argsPrefix+"POSTCOMMAND", "", "") } /* Shell command to run this test: go test -v ./local_test.go -args \ --LOCAL_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --LOCAL_INPUTKEYPATH="/path/to/your-input-key.pem" \ --LOCAL_OUTPUTCERTPATH="/path/to/your-output-cert" \ --LOCAL_OUTPUTKEYPATH="/path/to/your-output-key" \ --LOCAL_PFXPASSWORD="your-pfx-password" \ --LOCAL_JKSALIAS="your-jks-alias" \ --LOCAL_JKSKEYPASS="your-jks-keypass" \ --LOCAL_JKSSTOREPASS="your-jks-storepass" \ --LOCAL_SHELLENV="sh" \ --LOCAL_PRECOMMAND="echo 'hello world'" \ --LOCAL_POSTCOMMAND="echo 'bye-bye world'" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy_PEM", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("OUTPUTCERTPATH: %v", fOutputCertPath), fmt.Sprintf("OUTPUTKEYPATH: %v", fOutputKeyPath), fmt.Sprintf("SHELLENV: %v", fShellEnv), fmt.Sprintf("PRECOMMAND: %v", fPreCommand), fmt.Sprintf("POSTCOMMAND: %v", fPostCommand), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ OutputFormat: provider.OUTPUT_FORMAT_PEM, OutputCertPath: fOutputCertPath + ".pem", OutputKeyPath: fOutputKeyPath + ".pem", ShellEnv: fShellEnv, PreCommand: fPreCommand, PostCommand: fPostCommand, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } fstat1, err := os.Stat(fOutputCertPath + ".pem") if err != nil { t.Errorf("err: %+v", err) return } else if fstat1.Size() == 0 { t.Errorf("err: empty output certificate file") return } fstat2, err := os.Stat(fOutputKeyPath + ".pem") if err != nil { t.Errorf("err: %+v", err) return } else if fstat2.Size() == 0 { t.Errorf("err: empty output private key file") return } t.Logf("ok: %v", res) }) t.Run("Deploy_PFX", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("OUTPUTCERTPATH: %v", fOutputCertPath), fmt.Sprintf("PFXPASSWORD: %v", fPfxPassword), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ OutputFormat: provider.OUTPUT_FORMAT_PFX, OutputCertPath: fOutputCertPath + ".pfx", PfxPassword: fPfxPassword, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } fstat, err := os.Stat(fOutputCertPath + ".pfx") if err != nil { t.Errorf("err: %+v", err) return } else if fstat.Size() == 0 { t.Errorf("err: empty output certificate file") return } t.Logf("ok: %v", res) }) t.Run("Deploy_JKS", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("OUTPUTCERTPATH: %v", fOutputCertPath), fmt.Sprintf("JKSALIAS: %v", fJksAlias), fmt.Sprintf("JKSKEYPASS: %v", fJksKeypass), fmt.Sprintf("JKSSTOREPASS: %v", fJksStorepass), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ OutputFormat: provider.OUTPUT_FORMAT_JKS, OutputCertPath: fOutputCertPath + ".jks", JksAlias: fJksAlias, JksKeypass: fJksKeypass, JksStorepass: fJksStorepass, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } fstat, err := os.Stat(fOutputCertPath + ".jks") if err != nil { t.Errorf("err: %+v", err) return } else if fstat.Size() == 0 { t.Errorf("err: empty output certificate file") return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/mohua-mvh/mohua_mvh.go ================================================ package mohuamvh import ( "context" "errors" "fmt" "log/slog" "strconv" mohuasdk "github.com/mohuatech/mohuacloud-go-sdk" mohuasdktypes "github.com/mohuatech/mohuacloud-go-sdk/types" "github.com/certimate-go/certimate/pkg/core/deployer" ) type DeployerConfig struct { // 嘿华云账号。 Username string `json:"username"` // 嘿华云 API 密钥。 ApiPassword string `json:"apiPassword"` // 虚拟主机 ID。 HostId string `json:"hostId"` // 域名 ID。 DomainId string `json:"domainId"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *mohuasdk.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.Username, config.ApiPassword) 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) (*deployer.DeployResult, error) { if d.config.HostId == "" { return nil, errors.New("config `hostId` is required") } if d.config.DomainId == "" { return nil, errors.New("config `domainId` is required") } domainId, err := strconv.ParseInt(d.config.DomainId, 10, 64) if err != nil { return nil, err } // 登录获取 Token _, err = d.sdkClient.Auth.Login("", "") if err != nil { return nil, fmt.Errorf("failed to login mohua: %w", err) } // 设置 SSL 证书 setSSLReq := &mohuasdktypes.SetSSLRequest{ ID: int(domainId), SSLCert: certPEM, SSLKey: privkeyPEM, } setSSLResp, err := d.sdkClient.VirtualHost.SetSSL(d.config.HostId, setSSLReq) d.logger.Debug("sdk request 'mvh.SetSSL'", slog.Any("request", setSSLReq), slog.Any("response", setSSLResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'mvh.SetSSL': %w", err) } return &deployer.DeployResult{}, nil } func createSDKClient(username, apiPassword string) (*mohuasdk.Client, error) { if username == "" { return nil, errors.New("mohua: invalid username") } if apiPassword == "" { return nil, errors.New("mohua: invalid api password") } client := mohuasdk.NewClient( mohuasdk.WithCredentials(username, apiPassword), ) return client, nil } ================================================ FILE: pkg/core/deployer/providers/mohua-mvh/mohua_mvh_test.go ================================================ package mohuamvh_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/mohua-mvh" ) var ( fInputCertPath string fInputKeyPath string fUsername string fApiPassword string fHostID string fDomainID string ) func init() { argsPrefix := "MOHUAMVH_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "") flag.StringVar(&fApiPassword, argsPrefix+"APIPASSWORD", "", "") flag.StringVar(&fHostID, argsPrefix+"HOSTID", "", "") flag.StringVar(&fDomainID, argsPrefix+"DOMAINID", "", "") } /* Shell command to run this test: go test -v ./mohuamvh_test.go -args \ --MOHUAMVH_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --MOHUAMVH_INPUTKEYPATH="/path/to/your-input-key.pem" \ --MOHUAMVH_USERNAME="your-username" \ --MOHUAMVH_APIPASSWORD="your-api-password" \ --MOHUAMVH_HOSTID="your-virtual-host-id" \ --MOHUAMVH_DOMAINID="your-domain-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("USERNAME: %v", fUsername), fmt.Sprintf("APIPASSWORD: %v", fApiPassword), fmt.Sprintf("HOSTID: %v", fHostID), fmt.Sprintf("DOMAINID: %v", fDomainID), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ Username: fUsername, ApiPassword: fApiPassword, HostId: fHostID, DomainId: fDomainID, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/netlify/consts.go ================================================ package netlify const ( // 资源类型:替换指定网站的证书。 RESOURCE_TYPE_WEBSITE = "website" ) ================================================ FILE: pkg/core/deployer/providers/netlify/netlify.go ================================================ package netlify import ( "context" "errors" "fmt" "log/slog" "github.com/certimate-go/certimate/pkg/core/deployer" netlifysdk "github.com/certimate-go/certimate/pkg/sdk3rd/netlify" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type DeployerConfig struct { // netlify API Token。 ApiToken string `json:"apiToken"` // 部署资源类型。 ResourceType string `json:"resourceType"` // netlify 网站 ID。 // 部署资源类型为 [RESOURCE_TYPE_WEBSITE] 时必填。 SiteId string `json:"siteId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *netlifysdk.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("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) (*deployer.DeployResult, error) { // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_WEBSITE: if err := d.deployToWebsite(ctx, certPEM, privkeyPEM); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToWebsite(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.SiteId == "" { return errors.New("config `siteId` is required") } // 提取服务器证书和中间证书 serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM) if err != nil { return fmt.Errorf("failed to extract certs: %w", err) } // 上传网站证书 // REF: https://open-api.netlify.com/#tag/sniCertificate/operation/provisionSiteTLSCertificate provisionSiteTLSCertificateReq := &netlifysdk.ProvisionSiteTLSCertificateParams{ Certificate: serverCertPEM, CACertificates: intermediaCertPEM, Key: privkeyPEM, } provisionSiteTLSCertificateResp, err := d.sdkClient.ProvisionSiteTLSCertificateWithContext(ctx, d.config.SiteId, provisionSiteTLSCertificateReq) d.logger.Debug("sdk request 'netlify.provisionSiteTLSCertificate'", slog.String("siteId", d.config.SiteId), slog.Any("request", provisionSiteTLSCertificateReq), slog.Any("response", provisionSiteTLSCertificateResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'netlify.provisionSiteTLSCertificate': %w", err) } return nil } func createSDKClient(apiToken string) (*netlifysdk.Client, error) { return netlifysdk.NewClient(apiToken) } ================================================ FILE: pkg/core/deployer/providers/netlify/netlify_test.go ================================================ package netlify_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/netlify" ) var ( fInputCertPath string fInputKeyPath string fApiToken string fSiteId string ) func init() { argsPrefix := "NETLIFY_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "") flag.StringVar(&fSiteId, argsPrefix+"SITEID", "", "") } /* Shell command to run this test: go test -v ./netlify_test.go -args \ --NETLIFY_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --NETLIFY_INPUTKEYPATH="/path/to/your-input-key.pem" \ --NETLIFY_APITOKEN="your-api-token" \ --NETLIFY_SITEID="your-site-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("APITOKEN: %v", fApiToken), fmt.Sprintf("SITEID: %v", fSiteId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ApiToken: fApiToken, ResourceType: provider.RESOURCE_TYPE_WEBSITE, SiteId: fSiteId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/nginxproxymanager/consts.go ================================================ package nginxproxymanager const ( AUTH_METHOD_PASSWORD = "password" AUTH_METHOD_TOKEN = "token" ) const ( // 资源类型:替换指定主机的证书。 RESOURCE_TYPE_HOST = "host" // 资源类型:替换指定证书。 RESOURCE_TYPE_CERTIFICATE = "certificate" ) const ( // 匹配模式:指定 ID。 HOST_MATCH_PATTERN_SPECIFIED = "specified" // 匹配模式:证书 SAN 匹配。 HOST_MATCH_PATTERN_CERTSAN = "certsan" ) const ( HOST_TYPE_PROXY = "proxy" HOST_TYPE_REDIRECTION = "redirection" HOST_TYPE_STREAM = "stream" HOST_TYPE_DEAD = "dead" ) ================================================ FILE: pkg/core/deployer/providers/nginxproxymanager/nginxproxymanager.go ================================================ package nginxproxymanager import ( "context" "crypto/tls" "errors" "fmt" "log/slog" "strconv" "time" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/nginxproxymanager" "github.com/certimate-go/certimate/pkg/core/deployer" npmsdk "github.com/certimate-go/certimate/pkg/sdk3rd/nginxproxymanager" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xwait "github.com/certimate-go/certimate/pkg/utils/wait" ) type DeployerConfig struct { // NPM 服务地址。 ServerUrl string `json:"serverUrl"` // NPM API 认证方式。 // 可取值 "password"、"token"。 // 零值时默认值 [AUTH_METHOD_PASSWORD]。 AuthMethod string `json:"authMethod,omitempty"` // NPM 用户名。 Username string `json:"username,omitempty"` // NPM 密码。 Password string `json:"password,omitempty"` // NPM API Token。 ApiToken string `json:"apiToken,omitempty"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 域名匹配模式。 // 零值时默认值 [HOST_MATCH_PATTERN_SPECIFIED]。 HostMatchPattern string `json:"hostMatchPattern,omitempty"` // 主机类型。 // 部署资源类型为 [RESOURCE_TYPE_HOST] 时必填。 HostType string `json:"hostType,omitempty"` // 主机 ID。 // 部署资源类型为 [RESOURCE_TYPE_HOST]、且匹配模式非 [HOST_MATCH_PATTERN_CERTSAN] 时必填。 HostId int64 `json:"hostId,omitempty"` // 证书 ID。 // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。 CertificateId int64 `json:"certificateId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *npmsdk.Client sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ServerUrl, config.AuthMethod, config.Username, config.Password, config.ApiToken, config.AllowInsecureConnections) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ ServerUrl: config.ServerUrl, AuthMethod: config.AuthMethod, Username: config.Username, Password: config.Password, ApiToken: config.ApiToken, AllowInsecureConnections: config.AllowInsecureConnections, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_HOST: if err := d.deployToHost(ctx, certPEM, privkeyPEM); err != nil { return nil, err } case RESOURCE_TYPE_CERTIFICATE: if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToHost(ctx context.Context, certPEM, privkeyPEM string) error { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return err } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的主机列表 var hostIds []int64 switch d.config.HostMatchPattern { case "", HOST_MATCH_PATTERN_SPECIFIED: { if d.config.HostId == 0 { return errors.New("config `hostId` is required") } hostIds = []int64{d.config.HostId} } case HOST_MATCH_PATTERN_CERTSAN: { hostCandidates, err := d.getAllHosts(ctx, d.config.HostType) if err != nil { return err } hostIds = lo.Map( lo.Filter(hostCandidates, func(hostItem *npmsdk.HostRecord, _ int) bool { return len(hostItem.DomainNames) > 0 && lo.EveryBy(hostItem.DomainNames, func(domain string) bool { return certX509.VerifyHostname(domain) == nil }) }), func(hostItem *npmsdk.HostRecord, _ int) int64 { return hostItem.Id }, ) if len(hostIds) == 0 { return errors.New("could not find any hosts matched by certificate") } // 跳过已部署过的主机 hostIds = lo.Filter(hostIds, func(hostId int64, _ int) bool { hostInfo, _ := lo.Find(hostCandidates, func(hostItem *npmsdk.HostRecord) bool { return hostId == hostItem.Id }) if hostInfo != nil { return strconv.FormatInt(hostInfo.CertificateId, 10) != upres.CertId } return true }) } default: return fmt.Errorf("unsupported host match pattern: '%s'", d.config.HostMatchPattern) } // 遍历更新主机证书 if len(hostIds) == 0 { d.logger.Info("no hosts to deploy") } else { d.logger.Info("found hosts to deploy", slog.Any("hostIds", hostIds)) var errs []error certId, _ := strconv.ParseInt(upres.CertId, 10, 64) for i, hostId := range hostIds { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateHostCertificate(ctx, d.config.HostType, hostId, certId); err != nil { errs = append(errs, err) } if i < len(hostIds)-1 { xwait.DelayWithContext(ctx, time.Second*5) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.CertificateId == 0 { return errors.New("config `certificateId` is required") } // 替换证书 opres, err := d.sdkCertmgr.Replace(ctx, strconv.FormatInt(d.config.CertificateId, 10), certPEM, privkeyPEM) if err != nil { return fmt.Errorf("failed to replace certificate file: %w", err) } else { d.logger.Info("ssl certificate replaced", slog.Any("result", opres)) } // 获取默认站点 settingsGetDefaultSiteReq := &npmsdk.SettingsGetDefaultSiteRequest{} settingsGetDefaultSiteResp, err := d.sdkClient.SettingsGetDefaultSiteWithContext(ctx, settingsGetDefaultSiteReq) d.logger.Debug("sdk request 'settings.GetDefaultSite'", slog.Any("request", settingsGetDefaultSiteReq), slog.Any("response", settingsGetDefaultSiteResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'settings.GetDefaultSite': %w", err) } // 更新默认站点,以触发 nginx 重启 settingsSetDefaultSiteReq := &npmsdk.SettingsSetDefaultSiteRequest{ Value: settingsGetDefaultSiteResp.Value, } settingsSetDefaultSiteResp, err := d.sdkClient.SettingsSetDefaultSiteWithContext(ctx, settingsSetDefaultSiteReq) d.logger.Debug("sdk request 'settings.SetDefaultSite'", slog.Any("request", settingsSetDefaultSiteReq), slog.Any("response", settingsSetDefaultSiteResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'settings.SetDefaultSite': %w", err) } return nil } func (d *Deployer) getAllHosts(ctx context.Context, cloudHostType string) ([]*npmsdk.HostRecord, error) { var hosts []*npmsdk.HostRecord switch cloudHostType { case HOST_TYPE_PROXY: { nginxListProxyHostsReq := &npmsdk.NginxListProxyHostsRequest{} nginxListProxyHostsResp, err := d.sdkClient.NginxListProxyHostsWithContext(ctx, nginxListProxyHostsReq) d.logger.Debug("sdk request 'nginx.ListProxyHosts'", slog.Any("request", nginxListProxyHostsReq), slog.Any("response", nginxListProxyHostsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'nginx.ListProxyHosts': %w", err) } hosts = make([]*npmsdk.HostRecord, 0, len(*nginxListProxyHostsResp)) for _, hostItem := range *nginxListProxyHostsResp { hosts = append(hosts, &hostItem.HostRecord) } } case HOST_TYPE_REDIRECTION: { nginxListRedirectionHostsReq := &npmsdk.NginxListRedirectionHostsRequest{} nginxListRedirectionHostsResp, err := d.sdkClient.NginxListRedirectionHostsWithContext(ctx, nginxListRedirectionHostsReq) d.logger.Debug("sdk request 'nginx.ListRedirectionHosts'", slog.Any("request", nginxListRedirectionHostsReq), slog.Any("response", nginxListRedirectionHostsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'nginx.ListRedirectionHosts': %w", err) } hosts = make([]*npmsdk.HostRecord, 0, len(*nginxListRedirectionHostsResp)) for _, hostItem := range *nginxListRedirectionHostsResp { hosts = append(hosts, &hostItem.HostRecord) } } case HOST_TYPE_STREAM: { nginxListStreamsReq := &npmsdk.NginxListStreamsRequest{} nginxListStreamsResp, err := d.sdkClient.NginxListStreamsWithContext(ctx, nginxListStreamsReq) d.logger.Debug("sdk request 'nginx.ListStreams'", slog.Any("request", nginxListStreamsReq), slog.Any("response", nginxListStreamsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'nginx.ListStreams': %w", err) } hosts = make([]*npmsdk.HostRecord, 0, len(*nginxListStreamsResp)) for _, hostItem := range *nginxListStreamsResp { hosts = append(hosts, &hostItem.HostRecord) } } case HOST_TYPE_DEAD: { nginxListDeadHostsReq := &npmsdk.NginxListDeadHostsRequest{} nginxListDeadHostsResp, err := d.sdkClient.NginxListDeadHostsWithContext(ctx, nginxListDeadHostsReq) d.logger.Debug("sdk request 'nginx.ListDeadHosts'", slog.Any("request", nginxListDeadHostsReq), slog.Any("response", nginxListDeadHostsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'nginx.ListDeadHosts': %w", err) } hosts = make([]*npmsdk.HostRecord, 0, len(*nginxListDeadHostsResp)) for _, hostItem := range *nginxListDeadHostsResp { hosts = append(hosts, &hostItem.HostRecord) } } default: return hosts, fmt.Errorf("unsupported host type: '%s'", cloudHostType) } return hosts, nil } func (d *Deployer) updateHostCertificate(ctx context.Context, cloudHostType string, cloudHostId int64, cloudCertId int64) error { switch cloudHostType { case HOST_TYPE_PROXY: { nginxUpdateProxyHostReq := &npmsdk.NginxUpdateProxyHostRequest{ CertificateId: lo.ToPtr(cloudCertId), } nginxUpdateProxyHostResp, err := d.sdkClient.NginxUpdateProxyHostWithContext(ctx, cloudHostId, nginxUpdateProxyHostReq) d.logger.Debug("sdk request 'nginx.UpdateProxyHost'", slog.Int64("request.hostId", cloudHostId), slog.Any("request", nginxUpdateProxyHostReq), slog.Any("response", nginxUpdateProxyHostResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'nginx.UpdateProxyHost': %w", err) } } case HOST_TYPE_REDIRECTION: { nginxUpdateRedirectionHostReq := &npmsdk.NginxUpdateRedirectionHostRequest{ CertificateId: lo.ToPtr(cloudCertId), } nginxUpdateRedirectionHostResp, err := d.sdkClient.NginxUpdateRedirectionHostWithContext(ctx, cloudHostId, nginxUpdateRedirectionHostReq) d.logger.Debug("sdk request 'nginx.UpdateRedirectionHost'", slog.Int64("request.hostId", cloudHostId), slog.Any("request", nginxUpdateRedirectionHostReq), slog.Any("response", nginxUpdateRedirectionHostResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'nginx.UpdateRedirectionHost': %w", err) } } case HOST_TYPE_STREAM: { nginxUpdateStreamReq := &npmsdk.NginxUpdateStreamRequest{ CertificateId: lo.ToPtr(cloudCertId), } nginxUpdateStreamResp, err := d.sdkClient.NginxUpdateStreamWithContext(ctx, cloudHostId, nginxUpdateStreamReq) d.logger.Debug("sdk request 'nginx.UpdateStream'", slog.Int64("request.hostId", cloudHostId), slog.Any("request", nginxUpdateStreamReq), slog.Any("response", nginxUpdateStreamResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'nginx.UpdateStream': %w", err) } } case HOST_TYPE_DEAD: { nginxUpdateDeadHostReq := &npmsdk.NginxUpdateDeadHostRequest{ CertificateId: lo.ToPtr(cloudCertId), } nginxUpdateDeadHostResp, err := d.sdkClient.NginxUpdateDeadHostWithContext(ctx, cloudHostId, nginxUpdateDeadHostReq) d.logger.Debug("sdk request 'nginx.UpdateDeadHost'", slog.Int64("request.hostId", cloudHostId), slog.Any("request", nginxUpdateDeadHostReq), slog.Any("response", nginxUpdateDeadHostResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'nginx.UpdateDeadHost': %w", err) } } default: return fmt.Errorf("unsupported host type: '%s'", cloudHostType) } return nil } func createSDKClient(serverUrl, authMethod, username, password, apiToken string, skipTlsVerify bool) (*npmsdk.Client, error) { var client *npmsdk.Client var err error switch authMethod { case "", AUTH_METHOD_PASSWORD: { client, err = npmsdk.NewClient(serverUrl, username, password) } case AUTH_METHOD_TOKEN: { client, err = npmsdk.NewClientWithJwtToken(serverUrl, apiToken) } } if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } ================================================ FILE: pkg/core/deployer/providers/nginxproxymanager/nginxproxymanager_test.go ================================================ package nginxproxymanager_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/nginxproxymanager" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fUsername string fPassword string fHostType string fHostId int64 ) func init() { argsPrefix := "NGINXPROXYMANAGER_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "") flag.StringVar(&fHostType, argsPrefix+"HOSTTYPE", "", "") flag.Int64Var(&fHostId, argsPrefix+"HOSTID", 0, "") } /* Shell command to run this test: go test -v ./nginxproxymanager_test.go -args \ --NGINXPROXYMANAGER_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --NGINXPROXYMANAGER_INPUTKEYPATH="/path/to/your-input-key.pem" \ --NGINXPROXYMANAGER_SERVERURL="http://127.0.0.1:20410" \ --NGINXPROXYMANAGER_USERNAME="your-username" \ --NGINXPROXYMANAGER_PASSWORD="your-password" \ --NGINXPROXYMANAGER_HOSTTYPE="proxy" \ --NGINXPROXYMANAGER_HOSTID="your-host-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("USERNAME: %v", fUsername), fmt.Sprintf("PASSWORD: %v", fPassword), fmt.Sprintf("HOSTTYPE: %v", fHostType), fmt.Sprintf("HOSTID: %v", fHostId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ServerUrl: fServerUrl, AuthMethod: provider.AUTH_METHOD_PASSWORD, Username: fUsername, Password: fPassword, AllowInsecureConnections: true, ResourceType: provider.RESOURCE_TYPE_HOST, HostType: fHostType, HostMatchPattern: provider.HOST_MATCH_PATTERN_SPECIFIED, HostId: fHostId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/proxmoxve/proxmoxve.go ================================================ package proxmoxve import ( "context" "crypto/tls" "errors" "fmt" "log/slog" "net/http" "net/url" "strings" "github.com/luthermonson/go-proxmox" "github.com/certimate-go/certimate/pkg/core/deployer" xhttp "github.com/certimate-go/certimate/pkg/utils/http" ) type DeployerConfig struct { // Proxmox VE 服务地址。 ServerUrl string `json:"serverUrl"` // Proxmox VE API Token。 ApiToken string `json:"apiToken"` // Proxmox VE API Token Secret。 ApiTokenSecret string `json:"apiTokenSecret,omitempty"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` // 集群节点名称。 NodeName string `json:"nodeName"` // 是否自动重启。 AutoRestart bool `json:"autoRestart"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *proxmox.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ServerUrl, config.ApiToken, config.ApiTokenSecret, config.AllowInsecureConnections) 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) (*deployer.DeployResult, error) { if d.config.NodeName == "" { return nil, errors.New("config `nodeName` is required") } // 获取节点信息 // REF: https://pve.proxmox.com/pve-docs/api-viewer/index.html#/nodes/{node} node, err := d.sdkClient.Node(ctx, d.config.NodeName) if err != nil { return nil, fmt.Errorf("failed to get node '%s': %w", d.config.NodeName, err) } // 上传自定义证书 // REF: https://pve.proxmox.com/pve-docs/api-viewer/index.html#/nodes/{node}/certificates/custom err = node.UploadCustomCertificate(ctx, &proxmox.CustomCertificate{ Certificates: certPEM, Key: privkeyPEM, Force: true, Restart: d.config.AutoRestart, }) if err != nil { return nil, fmt.Errorf("failed to upload custom certificate to node '%s': %w", node.Name, err) } return &deployer.DeployResult{}, nil } func createSDKClient(serverUrl, apiToken, apiTokenSecret string, skipTlsVerify bool) (*proxmox.Client, error) { if _, err := url.Parse(serverUrl); err != nil { return nil, errors.New("pve: invalid server url") } if apiToken == "" { return nil, errors.New("pve: invalid api token") } httpClient := &http.Client{ Transport: xhttp.NewDefaultTransport(), Timeout: http.DefaultClient.Timeout, } if skipTlsVerify { transport := xhttp.NewDefaultTransport() transport.DisableKeepAlives = true transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} httpClient.Transport = transport } client := proxmox.NewClient( strings.TrimRight(serverUrl, "/")+"/api2/json", proxmox.WithHTTPClient(httpClient), proxmox.WithAPIToken(apiToken, apiTokenSecret), ) return client, nil } ================================================ FILE: pkg/core/deployer/providers/proxmoxve/proxmoxve_test.go ================================================ package proxmoxve_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/proxmoxve" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fApiToken string fApiTokenSecret string fNodeName string ) func init() { argsPrefix := "PROXMOXVE_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "") flag.StringVar(&fApiTokenSecret, argsPrefix+"APITOKENSECRET", "", "") flag.StringVar(&fNodeName, argsPrefix+"NODENAME", "", "") } /* Shell command to run this test: go test -v ./proxmoxve_test.go -args \ --PROXMOXVE_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --PROXMOXVE_INPUTKEYPATH="/path/to/your-input-key.pem" \ --PROXMOXVE_SERVERURL="http://127.0.0.1:8006" \ --PROXMOXVE_APITOKEN="your-api-token" \ --PROXMOXVE_APITOKENSECRET="your-api-token-secret" \ --PROXMOXVE_NODENAME="your-cluster-node-name" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("APITOKEN: %v", fApiToken), fmt.Sprintf("APITOKENSECRET: %v", fApiTokenSecret), fmt.Sprintf("NODENAME: %v", fNodeName), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ServerUrl: fServerUrl, ApiToken: fApiToken, ApiTokenSecret: fApiTokenSecret, AllowInsecureConnections: true, NodeName: fNodeName, AutoRestart: true, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/qiniu-cdn/consts.go ================================================ package qiniucdn const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn.go ================================================ package qiniucdn import ( "context" "errors" "fmt" "log/slog" "strings" "github.com/qiniu/go-sdk/v7/auth" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/qiniu-sslcert" "github.com/certimate-go/certimate/pkg/core/deployer" qiniusdk "github.com/certimate-go/certimate/pkg/sdk3rd/qiniu" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 七牛云 AccessKey。 AccessKey string `json:"accessKey"` // 七牛云 SecretKey。 SecretKey string `json:"secretKey"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *qiniusdk.CdnManager sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client := qiniusdk.NewCdnManager(auth.New(config.AccessKey, config.SecretKey)) pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKey: config.AccessKey, SecretKey: config.SecretKey, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } // "*.example.com" → ".example.com",适配七牛云 CDN 要求的泛域名格式 domain := strings.TrimPrefix(d.config.Domain, "*") domains = []string{domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return xcerthostname.IsMatch(d.config.Domain, domain) || strings.TrimPrefix(d.config.Domain, "*") == strings.TrimPrefix(domain, "*") }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by wildcard") } } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil || strings.TrimPrefix(d.config.Domain, "*") == strings.TrimPrefix(domain, "*") }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no cdn domains to deploy") } else { d.logger.Info("found cdn domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 查询域名列表 // REF: https://developer.qiniu.com/fusion/4246/the-domain-name getDomainListMarker := "" for { select { case <-ctx.Done(): return nil, ctx.Err() default: } getDomainListResp, err := d.sdkClient.GetDomainList(ctx, getDomainListMarker, 100) d.logger.Debug("sdk request 'cdn.GetDomainList'", slog.String("request.marker", getDomainListMarker), slog.Any("response", getDomainListResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.GetDomainList': %w", err) } ignoredStatuses := []string{"frozen", "offlined"} for _, domainItem := range getDomainListResp.Domains { if lo.Contains(ignoredStatuses, domainItem.OperatingState) { continue } domains = append(domains, domainItem.Name) } if len(getDomainListResp.Domains) == 0 || getDomainListResp.Marker == "" { break } getDomainListMarker = getDomainListResp.Marker } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error { // 获取域名信息 // REF: https://developer.qiniu.com/fusion/4246/the-domain-name getDomainInfoResp, err := d.sdkClient.GetDomainInfo(ctx, domain) d.logger.Debug("sdk request 'cdn.GetDomainInfo'", slog.String("request.domain", domain), slog.Any("response", getDomainInfoResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'cdn.GetDomainInfo': %w", err) } // 判断域名是否已启用 HTTPS // 如果已启用,修改域名证书;否则,启用 HTTPS // REF: https://developer.qiniu.com/fusion/4246/the-domain-name if getDomainInfoResp.Https == nil || getDomainInfoResp.Https.CertID == "" { enableDomainHttpsResp, err := d.sdkClient.EnableDomainHttps(ctx, domain, cloudCertId, true, true) d.logger.Debug("sdk request 'cdn.EnableDomainHttps'", slog.String("request.domain", domain), slog.String("request.certId", cloudCertId), slog.Any("response", enableDomainHttpsResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'cdn.EnableDomainHttps': %w", err) } } else if getDomainInfoResp.Https.CertID != cloudCertId { modifyDomainHttpsConfResp, err := d.sdkClient.ModifyDomainHttpsConf(ctx, domain, cloudCertId, getDomainInfoResp.Https.ForceHttps, getDomainInfoResp.Https.Http2Enable) d.logger.Debug("sdk request 'cdn.ModifyDomainHttpsConf'", slog.String("request.domain", domain), slog.String("request.certId", cloudCertId), slog.Any("response", modifyDomainHttpsConfResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'cdn.ModifyDomainHttpsConf': %w", err) } } return nil } ================================================ FILE: pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn_test.go ================================================ package qiniucdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/qiniu-cdn" ) var ( fInputCertPath string fInputKeyPath string fAccessKey string fSecretKey string fDomain string ) func init() { argsPrefix := "QINIUCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "") flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./qiniu_cdn_test.go -args \ --QINIUCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --QINIUCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --QINIUCDN_ACCESSKEY="your-access-key" \ --QINIUCDN_SECRETKEY="your-secret-key" \ --QINIUCDN_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEY: %v", fAccessKey), fmt.Sprintf("SECRETKEY: %v", fSecretKey), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKey: fAccessKey, SecretKey: fSecretKey, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/qiniu-kodo/qiniu_kodo.go ================================================ package qiniukodo import ( "context" "errors" "fmt" "log/slog" "github.com/qiniu/go-sdk/v7/auth" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/qiniu-sslcert" "github.com/certimate-go/certimate/pkg/core/deployer" qiniusdk "github.com/certimate-go/certimate/pkg/sdk3rd/qiniu" ) type DeployerConfig struct { // 七牛云 AccessKey。 AccessKey string `json:"accessKey"` // 七牛云 SecretKey。 SecretKey string `json:"secretKey"` // 存储桶名。暂时无用。 Bucket string `json:"bucket"` // 自定义域名(不支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *qiniusdk.KodoManager sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client := qiniusdk.NewKodoManager(auth.New(config.AccessKey, config.SecretKey)) pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKey: config.AccessKey, SecretKey: config.SecretKey, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { if d.config.Domain == "" { return nil, fmt.Errorf("config `domain` is required") } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 绑定空间域名证书 bindBucketCertResp, err := d.sdkClient.BindBucketCert(ctx, d.config.Domain, upres.CertId) d.logger.Debug("sdk request 'kodo.BindCert'", slog.String("request.domain", d.config.Domain), slog.String("request.certId", upres.CertId), slog.Any("response", bindBucketCertResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'kodo.BindCert': %w", err) } return &deployer.DeployResult{}, nil } ================================================ FILE: pkg/core/deployer/providers/qiniu-kodo/qiniu_kodo_test.go ================================================ package qiniukodo_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/qiniu-kodo" ) var ( fInputCertPath string fInputKeyPath string fAccessKey string fSecretKey string fBucket string fDomain string ) func init() { argsPrefix := "QINIUKODO_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "") flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") flag.StringVar(&fBucket, argsPrefix+"BUCKET", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./qiniu_kodo_test.go -args \ --QINIUKODO_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --QINIUKODO_INPUTKEYPATH="/path/to/your-input-key.pem" \ --QINIUKODO_ACCESSKEY="your-access-key" \ --QINIUKODO_SECRETKEY="your-secret-key" \ --QINIUKODO_BUCKET="your-bucket" \ --QINIUKODO_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEY: %v", fAccessKey), fmt.Sprintf("SECRETKEY: %v", fSecretKey), fmt.Sprintf("BUCKET: %v", fBucket), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKey: fAccessKey, SecretKey: fSecretKey, Bucket: fBucket, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/qiniu-pili/consts.go ================================================ package qiniupili const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/qiniu-pili/qiniu_pili.go ================================================ package qiniupili import ( "context" "errors" "fmt" "log/slog" "github.com/qiniu/go-sdk/v7/pili" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/qiniu-sslcert" "github.com/certimate-go/certimate/pkg/core/deployer" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type DeployerConfig struct { // 七牛云 AccessKey。 AccessKey string `json:"accessKey"` // 七牛云 SecretKey。 SecretKey string `json:"secretKey"` // 直播空间名。 Hub string `json:"hub"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 直播流域名(不支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *pili.Manager sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } manager := pili.NewManager(pili.ManagerConfig{AccessKey: config.AccessKey, SecretKey: config.SecretKey}) pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKey: config.AccessKey, SecretKey: config.SecretKey, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: manager, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { if d.config.Domain == "" { return nil, fmt.Errorf("config `domain` is required") } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomainsByHub(ctx, d.config.Hub) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no pili domains to deploy") } else { d.logger.Info("found pili domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, d.config.Hub, domain, upres.CertName); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomainsByHub(ctx context.Context, hub string) ([]string, error) { domains := make([]string, 0) // 查询域名列表 // REF: https://developer.qiniu.com/pili/9910/pili-service-sdk#6 getDomainListReq := pili.GetDomainsListRequest{ Hub: hub, } getDomainListResp, err := d.sdkClient.GetDomainsList(ctx, getDomainListReq) d.logger.Debug("sdk request 'pili.GetDomainsList'", slog.Any("request", getDomainListReq), slog.Any("response", getDomainListResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'pili.GetDomainsList': %w", err) } for _, domainItem := range getDomainListResp.Domains { domains = append(domains, domainItem.Domain) } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, hub string, domain string, cloudCertName string) error { // 修改域名证书配置 // REF: https://developer.qiniu.com/pili/9910/pili-service-sdk#6 setDomainCertReq := pili.SetDomainCertRequest{ Hub: hub, Domain: domain, CertName: cloudCertName, } err := d.sdkClient.SetDomainCert(ctx, setDomainCertReq) d.logger.Debug("sdk request 'pili.SetDomainCert'", slog.Any("request", setDomainCertReq)) if err != nil { return fmt.Errorf("failed to execute sdk request 'pili.SetDomainCert': %w", err) } return nil } ================================================ FILE: pkg/core/deployer/providers/qiniu-pili/qiniu_pili_test.go ================================================ package qiniupili_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/qiniu-pili" ) var ( fInputCertPath string fInputKeyPath string fAccessKey string fSecretKey string fHub string fDomain string ) func init() { argsPrefix := "QINIUPILI_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "") flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") flag.StringVar(&fHub, argsPrefix+"HUB", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./qiniu_pili_test.go -args \ --QINIUPILI_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --QINIUPILI_INPUTKEYPATH="/path/to/your-input-key.pem" \ --QINIUPILI_ACCESSKEY="your-access-key" \ --QINIUPILI_SECRETKEY="your-secret-key" \ --QINIUPILI_HUB="your-hub-name" \ --QINIUPILI_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEY: %v", fAccessKey), fmt.Sprintf("SECRETKEY: %v", fSecretKey), fmt.Sprintf("HUB: %v", fHub), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKey: fAccessKey, SecretKey: fSecretKey, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/rainyun-rcdn/consts.go ================================================ package rainyunrcdn const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" ) ================================================ FILE: pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn.go ================================================ package rainyunrcdn import ( "context" "errors" "fmt" "log/slog" "strconv" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/rainyun-sslcenter" "github.com/certimate-go/certimate/pkg/core/deployer" rainyunsdk "github.com/certimate-go/certimate/pkg/sdk3rd/rainyun" ) type DeployerConfig struct { // 雨云 API 密钥。 ApiKey string `json:"apiKey"` // RCDN 实例 ID。 InstanceId int64 `json:"instanceId"` // 域名匹配模式。暂时只支持精确匹配。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *rainyunsdk.Client sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ApiKey) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ ApiKey: config.ApiKey, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { if d.config.InstanceId == 0 { return nil, fmt.Errorf("config `instanceId` is required") } if d.config.Domain == "" { return nil, fmt.Errorf("config `domain` is required") } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // RCDN SSL 绑定域名 // REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-184214120 certId, _ := strconv.ParseInt(upres.CertId, 10, 64) rcdnInstanceSslBindReq := &rainyunsdk.RcdnInstanceSslBindRequest{ CertId: certId, Domains: []string{d.config.Domain}, } rcdnInstanceSslBindResp, err := d.sdkClient.RcdnInstanceSslBindWithContext(ctx, d.config.InstanceId, rcdnInstanceSslBindReq) d.logger.Debug("sdk request 'rcdn.InstanceSslBind'", slog.Int64("instanceId", d.config.InstanceId), slog.Any("request", rcdnInstanceSslBindReq), slog.Any("response", rcdnInstanceSslBindResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'rcdn.InstanceSslBind': %w", err) } return &deployer.DeployResult{}, nil } func createSDKClient(apiKey string) (*rainyunsdk.Client, error) { return rainyunsdk.NewClient(apiKey) } ================================================ FILE: pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn_test.go ================================================ package rainyunrcdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/rainyun-rcdn" ) var ( fInputCertPath string fInputKeyPath string fApiKey string fInstanceId int64 fDomain string ) func init() { argsPrefix := "RAINYUNRCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") flag.Int64Var(&fInstanceId, argsPrefix+"INSTANCEID", 0, "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./rainyun_rcdn_test.go -args \ --RAINYUNRCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --RAINYUNRCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --RAINYUNRCDN_APIKEY="your-api-key" \ --RAINYUNRCDN_INSTANCEID="your-rcdn-instance-id" \ --RAINYUNRCDN_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("APIKEY: %v", fApiKey), fmt.Sprintf("INSTANCEID: %v", fInstanceId), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ApiKey: fApiKey, InstanceId: fInstanceId, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/rainyun-sslcenter/rainyun_sslcenter.go ================================================ package rainyunsslcenter import ( "context" "errors" "fmt" "log/slog" "strconv" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/rainyun-sslcenter" "github.com/certimate-go/certimate/pkg/core/deployer" ) type DeployerConfig struct { // 雨云 API 密钥。 ApiKey string `json:"apiKey"` // 证书 ID。 // 选填。零值时表示新建证书;否则表示更新证书。 CertificateId int64 `json:"certificateId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ ApiKey: config.ApiKey, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { if d.config.CertificateId == 0 { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } } else { // 替换证书 opres, err := d.sdkCertmgr.Replace(ctx, strconv.FormatInt(d.config.CertificateId, 10), certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to replace certificate file: %w", err) } else { d.logger.Info("ssl certificate replaced", slog.Any("result", opres)) } } return &deployer.DeployResult{}, nil } ================================================ FILE: pkg/core/deployer/providers/rainyun-sslcenter/rainyun_sslcenter_test.go ================================================ package rainyunsslcenter_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/rainyun-sslcenter" ) var ( fInputCertPath string fInputKeyPath string fApiKey string ) func init() { argsPrefix := "RAINYUNSSLCENTER_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") } /* Shell command to run this test: go test -v ./rainyun_sslcenter_test.go -args \ --RAINYUNRCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --RAINYUNRCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --RAINYUNRCDN_APIKEY="your-api-key" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("APIKEY: %v", fApiKey), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ApiKey: fApiKey, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/ratpanel/consts.go ================================================ package ratpanel const ( // 资源类型:替换指定网站的证书。 RESOURCE_TYPE_WEBSITE = "website" // 资源类型:替换指定证书。 RESOURCE_TYPE_CERTIFICATE = "certificate" ) ================================================ FILE: pkg/core/deployer/providers/ratpanel/ratpanel.go ================================================ package ratpanel import ( "context" "crypto/tls" "errors" "fmt" "log/slog" "github.com/certimate-go/certimate/pkg/core/deployer" ratpanelsdk "github.com/certimate-go/certimate/pkg/sdk3rd/ratpanel" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type DeployerConfig struct { // 耗子面板服务地址。 ServerUrl string `json:"serverUrl"` // 耗子面板访问令牌 ID。 AccessTokenId int64 `json:"accessTokenId"` // 耗子面板访问令牌。 AccessToken string `json:"accessToken"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 网站名称。 // 部署资源类型为 [RESOURCE_TYPE_WEBSITE] 时必填。 SiteNames []string `json:"siteNames,omitempty"` // 证书 ID。 // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。 CertificateId int64 `json:"certificateId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *ratpanelsdk.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ServerUrl, config.AccessTokenId, config.AccessToken, config.AllowInsecureConnections) 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) (*deployer.DeployResult, error) { // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_WEBSITE: if err := d.deployToWebsite(ctx, certPEM, privkeyPEM); err != nil { return nil, err } case RESOURCE_TYPE_CERTIFICATE: if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToWebsite(ctx context.Context, certPEM, privkeyPEM string) error { if len(d.config.SiteNames) == 0 { return errors.New("config `siteNames` is required") } // 遍历更新站点证书 var errs []error for _, siteName := range d.config.SiteNames { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateSiteCertificate(ctx, siteName, certPEM, privkeyPEM); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.Join(errs...) } return nil } func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.CertificateId == 0 { return errors.New("config `certificateId` is required") } // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return err } // 更新 SSL 证书 certUpdateReq := &ratpanelsdk.CertUpdateRequest{ CertId: d.config.CertificateId, Type: "upload", Domains: certX509.DNSNames, Certificate: certPEM, PrivateKey: privkeyPEM, } certUpdateResp, err := d.sdkClient.CertUpdateWithContext(ctx, certUpdateReq) d.logger.Debug("sdk request 'ratpanel.CertUpdate'", slog.Any("request", certUpdateReq), slog.Any("response", certUpdateResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'ratpanel.CertUpdate': %w", err) } return nil } func (d *Deployer) updateSiteCertificate(ctx context.Context, siteName string, certPEM, privkeyPEM string) error { // 设置站点 SSL 证书 setWebsiteCertReq := &ratpanelsdk.SetWebsiteCertRequest{ SiteName: siteName, Certificate: certPEM, PrivateKey: privkeyPEM, } setWebsiteCertResp, err := d.sdkClient.SetWebsiteCertWithContext(ctx, setWebsiteCertReq) d.logger.Debug("sdk request 'ratpanel.SetWebsiteCert'", slog.Any("request", setWebsiteCertReq), slog.Any("response", setWebsiteCertResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'ratpanel.SetWebsiteCert': %w", err) } return nil } func createSDKClient(serverUrl string, accessTokenId int64, accessToken string, skipTlsVerify bool) (*ratpanelsdk.Client, error) { client, err := ratpanelsdk.NewClient(serverUrl, accessTokenId, accessToken) if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } ================================================ FILE: pkg/core/deployer/providers/ratpanel/ratpanel_test.go ================================================ package ratpanel_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/ratpanel" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fAccessTokenId int64 fAccessToken string fSiteName string ) func init() { argsPrefix := "RATPANEL_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.Int64Var(&fAccessTokenId, argsPrefix+"ACCESSTOKENID", 0, "") flag.StringVar(&fAccessToken, argsPrefix+"ACCESSTOKEN", "", "") flag.StringVar(&fSiteName, argsPrefix+"SITENAME", "", "") } /* Shell command to run this test: go test -v ./ratpanel_test.go -args \ --RATPANEL_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --RATPANEL_INPUTKEYPATH="/path/to/your-input-key.pem" \ --RATPANEL_SERVERURL="http://127.0.0.1:8888" \ --RATPANEL_ACCESSTOKENID="your-access-token-id" \ --RATPANEL_ACCESSTOKEN="your-access-token" \ --RATPANEL_SITENAME="your-site-name" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("ACCESSTOKENID: %v", fAccessTokenId), fmt.Sprintf("ACCESSTOKEN: %v", fAccessToken), fmt.Sprintf("SITENAME: %v", fSiteName), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ServerUrl: fServerUrl, AccessTokenId: fAccessTokenId, AccessToken: fAccessToken, AllowInsecureConnections: true, ResourceType: provider.RESOURCE_TYPE_WEBSITE, SiteNames: []string{fSiteName}, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/ratpanel-console/ratpanel_console.go ================================================ package ratpanelconsole import ( "context" "crypto/tls" "errors" "fmt" "log/slog" "github.com/certimate-go/certimate/pkg/core/deployer" ratpanelsdk "github.com/certimate-go/certimate/pkg/sdk3rd/ratpanel" ) type DeployerConfig struct { // 耗子面板服务地址。 ServerUrl string `json:"serverUrl"` // 耗子面板访问令牌 ID。 AccessTokenId int64 `json:"accessTokenId"` // 耗子面板访问令牌。 AccessToken string `json:"accessToken"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *ratpanelsdk.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ServerUrl, config.AccessTokenId, config.AccessToken, config.AllowInsecureConnections) 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) (*deployer.DeployResult, error) { // 设置面板 SSL 证书 setSettingCertReq := &ratpanelsdk.SetSettingCertRequest{ Certificate: certPEM, PrivateKey: privkeyPEM, } setSettingCertResp, err := d.sdkClient.SetSettingCertWithContext(ctx, setSettingCertReq) d.logger.Debug("sdk request 'ratpanel.SetSettingCert'", slog.Any("request", setSettingCertReq), slog.Any("response", setSettingCertResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'ratpanel.SetSettingCert': %w", err) } return &deployer.DeployResult{}, nil } func createSDKClient(serverUrl string, accessTokenId int64, accessToken string, skipTlsVerify bool) (*ratpanelsdk.Client, error) { client, err := ratpanelsdk.NewClient(serverUrl, accessTokenId, accessToken) if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } ================================================ FILE: pkg/core/deployer/providers/ratpanel-console/ratpanel_console_test.go ================================================ package ratpanelconsole_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/ratpanel-console" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fAccessTokenId int64 fAccessToken string ) func init() { argsPrefix := "RATPANELCONSOLE_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.Int64Var(&fAccessTokenId, argsPrefix+"ACCESSTOKENID", 0, "") flag.StringVar(&fAccessToken, argsPrefix+"ACCESSTOKEN", "", "") } /* Shell command to run this test: go test -v ./ratpanel_console_test.go -args \ --RATPANELCONSOLE_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --RATPANELCONSOLE_INPUTKEYPATH="/path/to/your-input-key.pem" \ --RATPANELCONSOLE_SERVERURL="http://127.0.0.1:8888" \ --RATPANELCONSOLE_ACCESSTOKENID="your-access-token-id" \ --RATPANELCONSOLE_ACCESSTOKEN="your-access-token" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("ACCESSTOKENID: %v", fAccessTokenId), fmt.Sprintf("ACCESSTOKEN: %v", fAccessToken), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ServerUrl: fServerUrl, AccessTokenId: fAccessTokenId, AccessToken: fAccessToken, AllowInsecureConnections: true, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/s3/consts.go ================================================ package s3 import ( "github.com/certimate-go/certimate/internal/domain" ) const ( OUTPUT_FORMAT_PEM = string(domain.CertificateFormatTypePEM) OUTPUT_FORMAT_PFX = string(domain.CertificateFormatTypePFX) OUTPUT_FORMAT_JKS = string(domain.CertificateFormatTypeJKS) ) ================================================ FILE: pkg/core/deployer/providers/s3/s3.go ================================================ package s3 import ( "context" "errors" "fmt" "log/slog" "github.com/certimate-go/certimate/internal/tools/s3" "github.com/certimate-go/certimate/pkg/core/deployer" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type DeployerConfig struct { // S3 Endpoint。 Endpoint string `json:"endpoint"` // S3 AccessKey。 AccessKey string `json:"accessKey"` // S3 SecretKey。 SecretKey string `json:"secretKey"` // S3 签名版本。 // 可取值 "v2"、"v4"。 // 零值时默认值 "v4"。 SignatureVersion string `json:"signatureVersion,omitempty"` // 是否使用路径风格。 UsePathStyle bool `json:"usePathStyle,omitempty"` // 存储区域。 Region string `json:"region"` // 存储桶名。 Bucket string `json:"bucket"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` // 输出证书格式。 OutputFormat string `json:"outputFormat,omitempty"` // 输出证书文件路径。 OutputCertObjectKey string `json:"outputCertObjectKey,omitempty"` // 输出服务器证书文件路径。 // 选填。 OutputServerCertObjectKey string `json:"outputServerCertObjectKey,omitempty"` // 输出中间证书文件路径。 // 选填。 OutputIntermediaCertObjectKey string `json:"outputIntermediaCertObjectKey,omitempty"` // 输出私钥文件路径。 OutputKeyObjectKey string `json:"outputKeyObjectKey,omitempty"` // PFX 导出密码。 // 证书格式为 PFX 时必填。 PfxPassword string `json:"pfxPassword,omitempty"` // JKS 别名。 // 证书格式为 JKS 时必填。 JksAlias string `json:"jksAlias,omitempty"` // JKS 密钥密码。 // 证书格式为 JKS 时必填。 JksKeypass string `json:"jksKeypass,omitempty"` // JKS 存储密码。 // 证书格式为 JKS 时必填。 JksStorepass string `json:"jksStorepass,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger s3Client *s3.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createS3Client(*config) if err != nil { return nil, fmt.Errorf("s3: failed to create S3 client: %w", err) } return &Deployer{ config: config, logger: slog.Default(), s3Client: 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) (*deployer.DeployResult, error) { // 提取服务器证书和中间证书 serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM) if err != nil { return nil, fmt.Errorf("failed to extract certs: %w", err) } // 写入证书和私钥文件 switch d.config.OutputFormat { case OUTPUT_FORMAT_PEM: { if err := d.s3Client.PutObjectString(ctx, d.config.Bucket, d.config.OutputCertObjectKey, certPEM); err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } d.logger.Info("ssl certificate file uploaded", slog.String("bucket", d.config.Bucket), slog.String("object", d.config.OutputCertObjectKey)) if d.config.OutputServerCertObjectKey != "" { if err := d.s3Client.PutObjectString(ctx, d.config.Bucket, d.config.OutputServerCertObjectKey, serverCertPEM); err != nil { return nil, fmt.Errorf("failed to upload server certificate file: %w", err) } d.logger.Info("ssl server certificate file uploaded", slog.String("bucket", d.config.Bucket), slog.String("object", d.config.OutputServerCertObjectKey)) } if d.config.OutputIntermediaCertObjectKey != "" { if err := d.s3Client.PutObjectString(ctx, d.config.Bucket, d.config.OutputIntermediaCertObjectKey, intermediaCertPEM); err != nil { return nil, fmt.Errorf("failed to upload intermedia certificate file: %w", err) } d.logger.Info("ssl intermedia certificate file uploaded", slog.String("bucket", d.config.Bucket), slog.String("object", d.config.OutputIntermediaCertObjectKey)) } if err := d.s3Client.PutObjectString(ctx, d.config.Bucket, d.config.OutputKeyObjectKey, privkeyPEM); err != nil { return nil, fmt.Errorf("failed to upload private key file: %w", err) } d.logger.Info("ssl private key file uploaded", slog.String("bucket", d.config.Bucket), slog.String("object", d.config.OutputKeyObjectKey)) } case OUTPUT_FORMAT_PFX: { pfxData, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, d.config.PfxPassword) if err != nil { return nil, fmt.Errorf("failed to transform certificate to PFX: %w", err) } d.logger.Info("ssl certificate transformed to pfx") if err := d.s3Client.PutObjectBytes(ctx, d.config.Bucket, d.config.OutputCertObjectKey, pfxData); err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } d.logger.Info("ssl certificate file uploaded", slog.String("bucket", d.config.Bucket), slog.String("object", d.config.OutputCertObjectKey)) } case OUTPUT_FORMAT_JKS: { jksData, err := xcert.TransformCertificateFromPEMToJKS(certPEM, privkeyPEM, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass) if err != nil { return nil, fmt.Errorf("failed to transform certificate to JKS: %w", err) } d.logger.Info("ssl certificate transformed to jks") if err := d.s3Client.PutObjectBytes(ctx, d.config.Bucket, d.config.OutputCertObjectKey, jksData); err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } d.logger.Info("ssl certificate file uploaded", slog.String("bucket", d.config.Bucket), slog.String("object", d.config.OutputCertObjectKey)) } default: return nil, fmt.Errorf("unsupported output format '%s'", d.config.OutputFormat) } return &deployer.DeployResult{}, nil } func createS3Client(config DeployerConfig) (*s3.Client, error) { clientCfg := s3.NewDefaultConfig() clientCfg.Endpoint = config.Endpoint clientCfg.AccessKey = config.AccessKey clientCfg.SecretKey = config.SecretKey clientCfg.SignatureVersion = config.SignatureVersion clientCfg.UsePathStyle = config.UsePathStyle clientCfg.Region = config.Region clientCfg.SkipTlsVerify = config.AllowInsecureConnections client, err := s3.NewClient(clientCfg) if err != nil { return nil, err } return client, err } ================================================ FILE: pkg/core/deployer/providers/safeline/consts.go ================================================ package safeline const ( // 资源类型:替换指定证书。 RESOURCE_TYPE_CERTIFICATE = "certificate" ) ================================================ FILE: pkg/core/deployer/providers/safeline/safeline.go ================================================ package safeline import ( "context" "crypto/tls" "errors" "fmt" "log/slog" "github.com/certimate-go/certimate/pkg/core/deployer" safelinesdk "github.com/certimate-go/certimate/pkg/sdk3rd/safeline" ) type DeployerConfig struct { // 雷池服务地址。 ServerUrl string `json:"serverUrl"` // 雷池 API Token。 ApiToken string `json:"apiToken"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 证书 ID。 // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。 CertificateId int64 `json:"certificateId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *safelinesdk.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ServerUrl, config.ApiToken, config.AllowInsecureConnections) 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) (*deployer.DeployResult, error) { // 根据部署资源类型决定部署方式`` switch d.config.ResourceType { case RESOURCE_TYPE_CERTIFICATE: if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error { if d.config.CertificateId == 0 { return errors.New("config `certificateId` is required") } // 更新证书 updateCertificateReq := &safelinesdk.UpdateCertificateRequest{ Id: d.config.CertificateId, Type: 2, Manual: &safelinesdk.CertificateManul{ Crt: certPEM, Key: privkeyPEM, }, } updateCertificateResp, err := d.sdkClient.UpdateCertificateWithContext(ctx, updateCertificateReq) d.logger.Debug("sdk request 'safeline.UpdateCertificate'", slog.Any("request", updateCertificateReq), slog.Any("response", updateCertificateResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'safeline.UpdateCertificate': %w", err) } return nil } func createSDKClient(serverUrl, apiToken string, skipTlsVerify bool) (*safelinesdk.Client, error) { client, err := safelinesdk.NewClient(serverUrl, apiToken) if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } ================================================ FILE: pkg/core/deployer/providers/safeline/safeline_test.go ================================================ package safeline_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/safeline" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fApiToken string fCertificateId int64 ) func init() { argsPrefix := "SAFELINE_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "") flag.Int64Var(&fCertificateId, argsPrefix+"CERTIFICATEID", 0, "") } /* Shell command to run this test: go test -v ./safeline_test.go -args \ --SAFELINE_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --SAFELINE_INPUTKEYPATH="/path/to/your-input-key.pem" \ --SAFELINE_SERVERURL="http://127.0.0.1:9443" \ --SAFELINE_APITOKEN="your-api-token" \ --SAFELINE_CERTIFICATEID="your-certificate-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("APITOKEN: %v", fApiToken), fmt.Sprintf("CERTIFICATEID: %v", fCertificateId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ServerUrl: fServerUrl, ApiToken: fApiToken, AllowInsecureConnections: true, ResourceType: provider.RESOURCE_TYPE_CERTIFICATE, CertificateId: fCertificateId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/ssh/consts.go ================================================ package ssh import ( "github.com/certimate-go/certimate/internal/domain" ) const ( OUTPUT_FORMAT_PEM = string(domain.CertificateFormatTypePEM) OUTPUT_FORMAT_PFX = string(domain.CertificateFormatTypePFX) OUTPUT_FORMAT_JKS = string(domain.CertificateFormatTypeJKS) ) ================================================ FILE: pkg/core/deployer/providers/ssh/ssh.go ================================================ package ssh import ( "context" "errors" "fmt" "log/slog" "strings" "github.com/certimate-go/certimate/internal/tools/ssh" "github.com/certimate-go/certimate/pkg/core/deployer" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xssh "github.com/certimate-go/certimate/pkg/utils/ssh" ) type ServerConfig struct { // SSH 主机。 // 零值时默认值 "localhost"。 SshHost string `json:"sshHost,omitempty"` // SSH 端口。 // 零值时默认值 22。 SshPort int32 `json:"sshPort,omitempty"` // SSH 认证方式。 // 可取值 "none"、"password"、"key"。 // 零值时根据有无密码或私钥字段决定。 SshAuthMethod string `json:"sshAuthMethod,omitempty"` // SSH 登录用户名。 // 零值时默认值 "root"。 SshUsername string `json:"sshUsername,omitempty"` // SSH 登录密码。 SshPassword string `json:"sshPassword,omitempty"` // SSH 登录私钥。 SshKey string `json:"sshKey,omitempty"` // SSH 登录私钥口令。 SshKeyPassphrase string `json:"sshKeyPassphrase,omitempty"` } type DeployerConfig struct { ServerConfig // 跳板机配置数组。 JumpServers []ServerConfig `json:"jumpServers,omitempty"` // 是否回退使用 SCP。 UseSCP bool `json:"useSCP,omitempty"` // 前置命令。 PreCommand string `json:"preCommand,omitempty"` // 后置命令。 PostCommand string `json:"postCommand,omitempty"` // 输出证书格式。 OutputFormat string `json:"outputFormat,omitempty"` // 输出私钥文件路径。 OutputKeyPath string `json:"outputKeyPath,omitempty"` // 输出证书文件路径。 OutputCertPath string `json:"outputCertPath,omitempty"` // 输出服务器证书文件路径。 // 选填。 OutputServerCertPath string `json:"outputServerCertPath,omitempty"` // 输出中间证书文件路径。 // 选填。 OutputIntermediaCertPath string `json:"outputIntermediaCertPath,omitempty"` // PFX 导出密码。 // 证书格式为 PFX 时必填。 PfxPassword string `json:"pfxPassword,omitempty"` // JKS 别名。 // 证书格式为 JKS 时必填。 JksAlias string `json:"jksAlias,omitempty"` // JKS 密钥密码。 // 证书格式为 JKS 时必填。 JksKeypass string `json:"jksKeypass,omitempty"` // JKS 存储密码。 // 证书格式为 JKS 时必填。 JksStorepass string `json:"jksStorepass,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } return &Deployer{ config: config, logger: slog.Default(), }, 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) (*deployer.DeployResult, error) { // 提取服务器证书和中间证书 serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM) if err != nil { return nil, fmt.Errorf("failed to extract certs: %w", err) } client, err := createSshClient(*d.config) if err != nil { return nil, fmt.Errorf("ssh: failed to create SSH client: %w", err) } d.logger.Info("ssh connected") // 执行前置命令 if d.config.PreCommand != "" { command := d.config.PreCommand command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}", d.config.OutputCertPath) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_SERVER_PATH}", d.config.OutputServerCertPath) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_INTERMEDIA_PATH}", d.config.OutputIntermediaCertPath) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_PRIVATEKEY_PATH}", d.config.OutputKeyPath) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_PFX_PASSWORD}", d.config.PfxPassword) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_ALIAS}", d.config.JksAlias) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_KEYPASS}", d.config.JksKeypass) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_STOREPASS}", d.config.JksStorepass) stdout, stderr, err := xssh.RunCommand(client.GetClient(), command) d.logger.Debug("run pre-command", slog.String("stdout", stdout), slog.String("stderr", stderr)) if err != nil { return nil, fmt.Errorf("failed to execute pre-command (stdout: %s, stderr: %s): %w ", stdout, stderr, err) } } // 上传证书和私钥文件 switch d.config.OutputFormat { case OUTPUT_FORMAT_PEM: { if err := xssh.WriteRemoteString(client.GetClient(), d.config.OutputCertPath, certPEM, d.config.UseSCP); err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } d.logger.Info("ssl certificate file uploaded", slog.String("path", d.config.OutputCertPath)) if d.config.OutputServerCertPath != "" { if err := xssh.WriteRemoteString(client.GetClient(), d.config.OutputServerCertPath, serverCertPEM, d.config.UseSCP); err != nil { return nil, fmt.Errorf("failed to save server certificate file: %w", err) } d.logger.Info("ssl server certificate file uploaded", slog.String("path", d.config.OutputServerCertPath)) } if d.config.OutputIntermediaCertPath != "" { if err := xssh.WriteRemoteString(client.GetClient(), d.config.OutputIntermediaCertPath, intermediaCertPEM, d.config.UseSCP); err != nil { return nil, fmt.Errorf("failed to save intermedia certificate file: %w", err) } d.logger.Info("ssl intermedia certificate file uploaded", slog.String("path", d.config.OutputIntermediaCertPath)) } if err := xssh.WriteRemoteString(client.GetClient(), d.config.OutputKeyPath, privkeyPEM, d.config.UseSCP); err != nil { return nil, fmt.Errorf("failed to upload private key file: %w", err) } d.logger.Info("ssl private key file uploaded", slog.String("path", d.config.OutputKeyPath)) } case OUTPUT_FORMAT_PFX: { pfxData, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, d.config.PfxPassword) if err != nil { return nil, fmt.Errorf("failed to transform certificate to PFX: %w", err) } d.logger.Info("ssl certificate transformed to pfx") if err := xssh.WriteRemote(client.GetClient(), d.config.OutputCertPath, pfxData, d.config.UseSCP); err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } d.logger.Info("ssl certificate file uploaded", slog.String("path", d.config.OutputCertPath)) } case OUTPUT_FORMAT_JKS: { jksData, err := xcert.TransformCertificateFromPEMToJKS(certPEM, privkeyPEM, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass) if err != nil { return nil, fmt.Errorf("failed to transform certificate to JKS: %w", err) } d.logger.Info("ssl certificate transformed to jks") if err := xssh.WriteRemote(client.GetClient(), d.config.OutputCertPath, jksData, d.config.UseSCP); err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } d.logger.Info("ssl certificate file uploaded", slog.String("path", d.config.OutputCertPath)) } default: return nil, fmt.Errorf("unsupported output format '%s'", d.config.OutputFormat) } // 执行后置命令 if d.config.PostCommand != "" { command := d.config.PostCommand command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}", d.config.OutputCertPath) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_SERVER_PATH}", d.config.OutputServerCertPath) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_INTERMEDIA_PATH}", d.config.OutputIntermediaCertPath) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_PRIVATEKEY_PATH}", d.config.OutputKeyPath) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_PFX_PASSWORD}", d.config.PfxPassword) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_ALIAS}", d.config.JksAlias) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_KEYPASS}", d.config.JksKeypass) command = strings.ReplaceAll(command, "${CERTIMATE_DEPLOYER_CMDVAR_JKS_STOREPASS}", d.config.JksStorepass) stdout, stderr, err := xssh.RunCommand(client.GetClient(), command) d.logger.Debug("run post-command", slog.String("stdout", stdout), slog.String("stderr", stderr)) if err != nil { return nil, fmt.Errorf("failed to execute post-command (stdout: %s, stderr: %s): %w ", stdout, stderr, err) } } return &deployer.DeployResult{}, nil } func createSshClient(config DeployerConfig) (*ssh.Client, error) { clientCfg := ssh.NewDefaultConfig() clientCfg.Host = config.SshHost clientCfg.Port = int(config.SshPort) clientCfg.AuthMethod = ssh.AuthMethodType(config.SshAuthMethod) clientCfg.Username = config.SshUsername clientCfg.Password = config.SshPassword clientCfg.Key = config.SshKey clientCfg.KeyPassphrase = config.SshKeyPassphrase for _, jumpServer := range config.JumpServers { jumpServerCfg := ssh.NewServerConfig() jumpServerCfg.Host = jumpServer.SshHost jumpServerCfg.Port = int(jumpServer.SshPort) jumpServerCfg.AuthMethod = ssh.AuthMethodType(jumpServer.SshAuthMethod) jumpServerCfg.Username = jumpServer.SshUsername jumpServerCfg.Password = jumpServer.SshPassword jumpServerCfg.Key = jumpServer.SshKey jumpServerCfg.KeyPassphrase = jumpServer.SshKeyPassphrase clientCfg.JumpServers = append(clientCfg.JumpServers, *jumpServerCfg) } client, err := ssh.NewClient(clientCfg) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/ssh/ssh_test.go ================================================ package ssh_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/ssh" ) var ( fInputCertPath string fInputKeyPath string fSshHost string fSshPort int64 fSshUsername string fSshPassword string fOutputCertPath string fOutputKeyPath string ) func init() { argsPrefix := "SSH_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fSshHost, argsPrefix+"SSHHOST", "", "") flag.Int64Var(&fSshPort, argsPrefix+"SSHPORT", 0, "") flag.StringVar(&fSshUsername, argsPrefix+"SSHUSERNAME", "", "") flag.StringVar(&fSshPassword, argsPrefix+"SSHPASSWORD", "", "") flag.StringVar(&fOutputCertPath, argsPrefix+"OUTPUTCERTPATH", "", "") flag.StringVar(&fOutputKeyPath, argsPrefix+"OUTPUTKEYPATH", "", "") } /* Shell command to run this test: go test -v ./ssh_test.go -args \ --SSH_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --SSH_INPUTKEYPATH="/path/to/your-input-key.pem" \ --SSH_SSHHOST="localhost" \ --SSH_SSHPORT=22 \ --SSH_SSHUSERNAME="root" \ --SSH_SSHPASSWORD="password" \ --SSH_OUTPUTCERTPATH="/path/to/your-output-cert.pem" \ --SSH_OUTPUTKEYPATH="/path/to/your-output-key.pem" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SSHHOST: %v", fSshHost), fmt.Sprintf("SSHPORT: %v", fSshPort), fmt.Sprintf("SSHUSERNAME: %v", fSshUsername), fmt.Sprintf("SSHPASSWORD: %v", fSshPassword), fmt.Sprintf("OUTPUTCERTPATH: %v", fOutputCertPath), fmt.Sprintf("OUTPUTKEYPATH: %v", fOutputKeyPath), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ ServerConfig: provider.ServerConfig{ SshHost: fSshHost, SshPort: int32(fSshPort), SshUsername: fSshUsername, SshPassword: fSshPassword, }, OutputFormat: provider.OUTPUT_FORMAT_PEM, OutputCertPath: fOutputCertPath, OutputKeyPath: fOutputKeyPath, }) if err != nil { t.Errorf("err: %+v", err) } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/synologydsm/synologydsm.go ================================================ package synologydsm import ( "context" "crypto/tls" "errors" "fmt" "log/slog" "time" "github.com/pquerna/otp/totp" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/deployer" dsmsdk "github.com/certimate-go/certimate/pkg/sdk3rd/synologydsm" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xwait "github.com/certimate-go/certimate/pkg/utils/wait" ) type DeployerConfig struct { // 群晖 DSM 服务地址。 ServerUrl string `json:"serverUrl"` // 群晖 DSM 用户名。 Username string `json:"username"` // 群晖 DSM 用户密码。 Password string `json:"password"` // 群晖 DSM 2FA TOTP 密钥。 TotpSecret string `json:"totpSecret,omitempty"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` // 证书 ID 或描述。 // 选填。零值时表示新建证书;否则表示更新证书。 CertificateIdOrDescription string `json:"certificateIdOrDesc,omitempty"` // 是否设为默认证书。 IsDefault bool `json:"isDefault,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *dsmsdk.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.ServerUrl, config.AllowInsecureConnections) 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.Default() } else { d.logger = logger } } func (d *Deployer) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) { // 提取服务器证书和中间证书 serverCertPEM, intermediateCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM) if err != nil { return nil, fmt.Errorf("failed to extract certs: %w", err) } // 如果启用了 TOTP,则等到下一个时间窗口后生成 OTP 动态密码 var otpCode string if d.config.TotpSecret != "" { now := time.Now() wait := time.Duration(30-now.Unix()%30) * time.Second if wait > 0 { wait = wait + 1*time.Second d.logger.Info("waiting for the next TOTP time step ...", slog.Int("wait", int(wait.Seconds()))) xwait.DelayWithContext(ctx, wait) } now = time.Now() otpCodeStr, err := totp.GenerateCode(d.config.TotpSecret, now) if err != nil { return nil, fmt.Errorf("failed to generate TOTP code: %w", err) } otpCode = otpCodeStr } // 登录到群晖 DSM loginReq := &dsmsdk.LoginRequest{ Account: d.config.Username, Password: d.config.Password, OtpCode: otpCode, } loginResp, err := d.sdkClient.Login(loginReq) d.logger.Debug("sdk request 'SYNO.API.Auth:login'", slog.Any("request", loginReq), slog.Any("response", loginResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'SYNO.API.Auth:login': %w", err) } defer func() { logoutResp, _ := d.sdkClient.Logout() d.logger.Debug("sdk request 'SYNO.API.Auth:logout'", slog.Any("response", logoutResp)) }() // 如果原证书 ID 或描述为空,则创建证书;否则更新证书。 if d.config.CertificateIdOrDescription == "" { // 导入证书 importCertificateReq := &dsmsdk.ImportCertificateRequest{ ID: "", Description: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()), Key: privkeyPEM, Cert: serverCertPEM, InterCert: intermediateCertPEM, AsDefault: d.config.IsDefault, } importCertificateResp, err := d.sdkClient.ImportCertificate(importCertificateReq) d.logger.Debug("sdk request 'SYNO.Core.Certificate:import'", slog.Any("request", importCertificateReq), slog.Any("response", importCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'SYNO.Core.Certificate:import': %w", err) } } else { // 查找证书列表,找到已有证书 var certInfo *dsmsdk.CertificateInfo listCertificatesResp, err := d.sdkClient.ListCertificates() d.logger.Debug("sdk request 'SYNO.Core.Certificate.CRT:list'", slog.Any("response", listCertificatesResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'SYNO.Core.Certificate.CRT:list': %w", err) } else { matchedCerts := lo.Filter(listCertificatesResp.Data.Certificates, func(certItem *dsmsdk.CertificateInfo, _ int) bool { return certItem.ID == d.config.CertificateIdOrDescription }) if len(matchedCerts) == 0 { matchedCerts = lo.Filter(listCertificatesResp.Data.Certificates, func(certItem *dsmsdk.CertificateInfo, _ int) bool { return certItem.Description == d.config.CertificateIdOrDescription }) } if len(matchedCerts) == 0 { return nil, fmt.Errorf("could not find certificate '%s'", d.config.CertificateIdOrDescription) } else { if len(matchedCerts) > 1 { d.logger.Warn("found several certificates matched '%s', using the first one") } certInfo = matchedCerts[0] } } // 导入证书 importCertificateReq := &dsmsdk.ImportCertificateRequest{ ID: certInfo.ID, Description: certInfo.Description, Key: privkeyPEM, Cert: serverCertPEM, InterCert: intermediateCertPEM, AsDefault: d.config.IsDefault || certInfo.IsDefault, } importCertificateResp, err := d.sdkClient.ImportCertificate(importCertificateReq) d.logger.Debug("sdk request 'SYNO.Core.Certificate:import'", slog.Any("request", importCertificateReq), slog.Any("response", importCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'SYNO.Core.Certificate:import': %w", err) } } if d.config.IsDefault { // 查找证书列表,找到默认证书 listCertificatesResp, err := d.sdkClient.ListCertificates() d.logger.Debug("sdk request 'SYNO.Core.Certificate.CRT:list'", slog.Any("response", listCertificatesResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'SYNO.Core.Certificate.CRT:list': %w", err) } else { var defaultCertId string for _, certItem := range listCertificatesResp.Data.Certificates { if certItem.IsDefault { defaultCertId = certItem.ID break } } if defaultCertId != "" { settings := make([]*dsmsdk.ServiceCertificateSetting, 0) for _, certItem := range listCertificatesResp.Data.Certificates { if certItem.ID == defaultCertId { continue } for _, service := range certItem.Services { settings = append(settings, &dsmsdk.ServiceCertificateSetting{ Service: service, CertID: defaultCertId, OldCertID: certItem.ID, }) } } // 应用到所有服务并重启 if len(settings) > 0 { setServiceCertificateReq := &dsmsdk.SetServiceCertificateRequest{ Settings: settings, } setServiceCertificateResp, err := d.sdkClient.SetServiceCertificate(setServiceCertificateReq) d.logger.Debug("sdk request 'SYNO.Core.Certificate.Service:set'", slog.Any("request", setServiceCertificateReq), slog.Any("response", setServiceCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'SYNO.Core.Certificate.Service:set': %w", err) } } } } } return &deployer.DeployResult{}, nil } func createSDKClient(serverUrl string, skipTlsVerify bool) (*dsmsdk.Client, error) { client, err := dsmsdk.NewClient(serverUrl) if err != nil { return nil, err } if skipTlsVerify { client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}) } return client, nil } ================================================ FILE: pkg/core/deployer/providers/synologydsm/synologydsm_test.go ================================================ package synologydsm_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/synologydsm" ) var ( fInputCertPath string fInputKeyPath string fServerUrl string fUsername string fPassword string fTotpSecret string fCertificateIdOrDesc string fIsDefault bool ) func init() { argsPrefix := "SYNOLOGYDSM_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "") flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "") flag.StringVar(&fTotpSecret, argsPrefix+"TOTPSECRET", "", "") flag.StringVar(&fCertificateIdOrDesc, argsPrefix+"CERTIFICATEIDORDESC", "", "") flag.BoolVar(&fIsDefault, argsPrefix+"ISDEFAULT", false, "") } /* Shell command to run this test: go test -v ./synology_dsm_test.go -args \ --SYNOLOGYDSM_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --SYNOLOGYDSM_INPUTKEYPATH="/path/to/your-input-key.pem" \ --SYNOLOGYDSM_SERVERURL="http://127.0.0.1:5000/" \ --SYNOLOGYDSM_USERNAME="admin" \ --SYNOLOGYDSM_PASSWORD="password" \ --SYNOLOGYDSM_CERTIFICATEIDORDESC="your-certificate-id-or-desc" \ --SYNOLOGYDSM_ISDEFAULT=true */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("USERNAME: %v", fUsername), fmt.Sprintf("PASSWORD: %v", fPassword), fmt.Sprintf("TOTPSECRET: %v", fTotpSecret), fmt.Sprintf("CERTIFICATEIDORDESC: %v", fCertificateIdOrDesc), fmt.Sprintf("ISDEFAULT: %v", fIsDefault), }, "\n")) deployer, err := provider.NewDeployer(&provider.DeployerConfig{ ServerUrl: fServerUrl, Username: fUsername, Password: fPassword, TotpSecret: fTotpSecret, AllowInsecureConnections: true, CertificateIdOrDescription: fCertificateIdOrDesc, IsDefault: fIsDefault, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-cdn/consts.go ================================================ package tencentcloudcdn const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/tencentcloud-cdn/internal/client.go ================================================ package internal import ( "context" "errors" tccdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" ) // This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/cdn/v20180606/client.go // to lightweight the vendor packages in the built binary. type CdnClient struct { common.Client } func NewCdnClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *CdnClient, err error) { client = &CdnClient{} client.Init(region). WithCredential(credential). WithProfile(clientProfile) return } func (c *CdnClient) DescribeCertDomains(request *tccdn.DescribeCertDomainsRequest) (response *tccdn.DescribeCertDomainsResponse, err error) { return c.DescribeCertDomainsWithContext(context.Background(), request) } func (c *CdnClient) DescribeCertDomainsWithContext(ctx context.Context, request *tccdn.DescribeCertDomainsRequest) (response *tccdn.DescribeCertDomainsResponse, err error) { if request == nil { request = tccdn.NewDescribeCertDomainsRequest() } c.InitBaseRequest(&request.BaseRequest, "cdn", tccdn.APIVersion, "DescribeCertDomains") if c.GetCredential() == nil { return nil, errors.New("DescribeCertDomains require credential") } request.SetContext(ctx) response = tccdn.NewDescribeCertDomainsResponse() err = c.Send(request, response) return } func (c *CdnClient) DescribeDomains(request *tccdn.DescribeDomainsRequest) (response *tccdn.DescribeDomainsResponse, err error) { return c.DescribeDomainsWithContext(context.Background(), request) } func (c *CdnClient) DescribeDomainsWithContext(ctx context.Context, request *tccdn.DescribeDomainsRequest) (response *tccdn.DescribeDomainsResponse, err error) { if request == nil { request = tccdn.NewDescribeDomainsRequest() } c.InitBaseRequest(&request.BaseRequest, "cdn", tccdn.APIVersion, "DescribeDomains") if c.GetCredential() == nil { return nil, errors.New("DescribeDomains require credential") } request.SetContext(ctx) response = tccdn.NewDescribeDomainsResponse() err = c.Send(request, response) return } func (c *CdnClient) DescribeDomainsConfig(request *tccdn.DescribeDomainsConfigRequest) (response *tccdn.DescribeDomainsConfigResponse, err error) { return c.DescribeDomainsConfigWithContext(context.Background(), request) } func (c *CdnClient) DescribeDomainsConfigWithContext(ctx context.Context, request *tccdn.DescribeDomainsConfigRequest) (response *tccdn.DescribeDomainsConfigResponse, err error) { if request == nil { request = tccdn.NewDescribeDomainsConfigRequest() } c.InitBaseRequest(&request.BaseRequest, "cdn", tccdn.APIVersion, "DescribeDomainsConfig") if c.GetCredential() == nil { return nil, errors.New("DescribeDomainsConfig require credential") } request.SetContext(ctx) response = tccdn.NewDescribeDomainsConfigResponse() err = c.Send(request, response) return } func (c *CdnClient) UpdateDomainConfig(request *tccdn.UpdateDomainConfigRequest) (response *tccdn.UpdateDomainConfigResponse, err error) { return c.UpdateDomainConfigWithContext(context.Background(), request) } func (c *CdnClient) UpdateDomainConfigWithContext(ctx context.Context, request *tccdn.UpdateDomainConfigRequest) (response *tccdn.UpdateDomainConfigResponse, err error) { if request == nil { request = tccdn.NewUpdateDomainConfigRequest() } c.InitBaseRequest(&request.BaseRequest, "cdn", tccdn.APIVersion, "UpdateDomainConfig") if c.GetCredential() == nil { return nil, errors.New("UpdateDomainConfig require credential") } request.SetContext(ctx) response = tccdn.NewUpdateDomainConfigResponse() err = c.Send(request, response) return } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go ================================================ package tencentcloudcdn import ( "context" "errors" "fmt" "log/slog" "strings" "github.com/samber/lo" tccdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-cdn/internal" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 腾讯云 SecretId。 SecretId string `json:"secretId"` // 腾讯云 SecretKey。 SecretKey string `json:"secretKey"` // 腾讯云接口端点。 Endpoint string `json:"endpoint,omitempty"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.CdnClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ SecretId: config.SecretId, SecretKey: config.SecretKey, Endpoint: lo. If(strings.HasSuffix(config.Endpoint, "intl.tencentcloudapi.com"), "ssl.intl.tencentcloudapi.com"). // 国际站使用独立的接口端点 Else(""), }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的 CDN 实例 domains := make([]string, 0) switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getMatchedDomainsByWildcard(ctx, d.config.Domain) if err != nil { return nil, err } domains = domainCandidates } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { domainCandidates, err := d.getMatchedDomainsByCertId(ctx, upres.CertId) if err != nil { return nil, err } domains = domainCandidates } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no cdn domains to deploy") } else { d.logger.Info("found cdn domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getMatchedDomainsByWildcard(ctx context.Context, wildcardDomain string) ([]string, error) { domains := make([]string, 0) // 查询域名基本信息,获取匹配的域名 // REF: https://cloud.tencent.com/document/api/228/41118 describeDomainsOffset := 0 describeDomainsLimit := 100 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } describeDomainsReq := tccdn.NewDescribeDomainsRequest() describeDomainsReq.Filters = []*tccdn.DomainFilter{ { Name: common.StringPtr("domain"), Value: common.StringPtrs([]string{strings.TrimPrefix(wildcardDomain, "*.")}), Fuzzy: common.BoolPtr(true), }, } describeDomainsReq.Offset = common.Int64Ptr(int64(describeDomainsOffset)) describeDomainsReq.Limit = common.Int64Ptr(int64(describeDomainsLimit)) describeDomainsResp, err := d.sdkClient.DescribeDomains(describeDomainsReq) d.logger.Debug("sdk request 'cdn.DescribeDomains'", slog.Any("request", describeDomainsReq), slog.Any("response", describeDomainsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.DescribeDomains': %w", err) } if describeDomainsResp.Response == nil { break } for _, domainItem := range describeDomainsResp.Response.Domains { if lo.FromPtr(domainItem.Product) == "cdn" && xcerthostname.IsMatch(wildcardDomain, lo.FromPtr(domainItem.Domain)) { domains = append(domains, lo.FromPtr(domainItem.Domain)) } } if len(describeDomainsResp.Response.Domains) < describeDomainsLimit { break } describeDomainsOffset += describeDomainsLimit } return domains, nil } func (d *Deployer) getMatchedDomainsByCertId(ctx context.Context, cloudCertId string) ([]string, error) { // 获取证书中的可用域名 // REF: https://cloud.tencent.com/document/api/228/42491 describeCertDomainsReq := tccdn.NewDescribeCertDomainsRequest() describeCertDomainsReq.CertId = common.StringPtr(cloudCertId) describeCertDomainsReq.Product = common.StringPtr("cdn") describeCertDomainsResp, err := d.sdkClient.DescribeCertDomains(describeCertDomainsReq) d.logger.Debug("sdk request 'cdn.DescribeCertDomains'", slog.Any("request", describeCertDomainsReq), slog.Any("response", describeCertDomainsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.DescribeCertDomains': %w", err) } domains := make([]string, 0) if describeCertDomainsResp.Response.Domains != nil { for _, domain := range describeCertDomainsResp.Response.Domains { domains = append(domains, *domain) } } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error { // 查询域名详细配置 // REF: https://cloud.tencent.com/document/api/228/41117 describeDomainsConfigReq := tccdn.NewDescribeDomainsConfigRequest() describeDomainsConfigReq.Filters = []*tccdn.DomainFilter{ { Name: common.StringPtr("domain"), Value: common.StringPtrs([]string{domain}), }, } describeDomainsConfigReq.Offset = common.Int64Ptr(0) describeDomainsConfigReq.Limit = common.Int64Ptr(1) describeDomainsConfigResp, err := d.sdkClient.DescribeDomainsConfig(describeDomainsConfigReq) d.logger.Debug("sdk request 'cdn.DescribeDomainsConfig'", slog.Any("request", describeDomainsConfigReq), slog.Any("response", describeDomainsConfigResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'cdn.DescribeDomainsConfig': %w", err) } else if len(describeDomainsConfigResp.Response.Domains) == 0 { return fmt.Errorf("could not find domain '%s'", domain) } domainConfig := describeDomainsConfigResp.Response.Domains[0] if domainConfig.Https != nil && domainConfig.Https.CertInfo != nil && domainConfig.Https.CertInfo.CertId != nil && *domainConfig.Https.CertInfo.CertId == cloudCertId { // 已部署过此域名,跳过 return nil } // 更新加速域名配置 // REF: https://cloud.tencent.com/document/api/228/41116 updateDomainConfigReq := tccdn.NewUpdateDomainConfigRequest() updateDomainConfigReq.Domain = common.StringPtr(domain) updateDomainConfigReq.Https = domainConfig.Https if updateDomainConfigReq.Https == nil { updateDomainConfigReq.Https = &tccdn.Https{Switch: common.StringPtr("on")} } else { updateDomainConfigReq.Https.SslStatus = nil } updateDomainConfigReq.Https.CertInfo = &tccdn.ServerCert{ CertId: common.StringPtr(cloudCertId), } updateDomainConfigResp, err := d.sdkClient.UpdateDomainConfig(updateDomainConfigReq) d.logger.Debug("sdk request 'cdn.UpdateDomainConfig'", slog.Any("request", updateDomainConfigReq), slog.Any("response", updateDomainConfigResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'cdn.UpdateDomainConfig': %w", err) } return nil } func createSDKClient(secretId, secretKey, endpoint string) (*internal.CdnClient, error) { credential := common.NewCredential(secretId, secretKey) cpf := profile.NewClientProfile() if endpoint != "" { cpf.HttpProfile.Endpoint = endpoint } client, err := internal.NewCdnClient(credential, "", cpf) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-cdn/tencentcloud_cdn_test.go ================================================ package tencentcloudcdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-cdn" ) var ( fInputCertPath string fInputKeyPath string fSecretId string fSecretKey string fDomain string ) func init() { argsPrefix := "TENCENTCLOUDCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "") flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./tencentcloud_cdn_test.go -args \ --TENCENTCLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --TENCENTCLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --TENCENTCLOUDCDN_SECRETID="your-secret-id" \ --TENCENTCLOUDCDN_SECRETKEY="your-secret-key" \ --TENCENTCLOUDCDN_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SECRETID: %v", fSecretId), fmt.Sprintf("SECRETKEY: %v", fSecretKey), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ SecretId: fSecretId, SecretKey: fSecretKey, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-clb/consts.go ================================================ package tencentcloudclb const ( // 资源类型:部署到指定负载均衡器。 RESOURCE_TYPE_LOADBALANCER = "loadbalancer" // 资源类型:部署到指定监听器。 RESOURCE_TYPE_LISTENER = "listener" // 资源类型:部署到指定转发规则域名。 RESOURCE_TYPE_RULEDOMAIN = "ruledomain" ) ================================================ FILE: pkg/core/deployer/providers/tencentcloud-clb/internal/client.go ================================================ package internal import ( "context" "errors" tcclb "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb/v20180317" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" ) // This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/clb/v20180317/client.go // to lightweight the vendor packages in the built binary. type ClbClient struct { common.Client } func NewClbClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *ClbClient, err error) { client = &ClbClient{} client.Init(region). WithCredential(credential). WithProfile(clientProfile) return } func (c *ClbClient) DescribeListeners(request *tcclb.DescribeListenersRequest) (response *tcclb.DescribeListenersResponse, err error) { return c.DescribeListenersWithContext(context.Background(), request) } func (c *ClbClient) DescribeListenersWithContext(ctx context.Context, request *tcclb.DescribeListenersRequest) (response *tcclb.DescribeListenersResponse, err error) { if request == nil { request = tcclb.NewDescribeListenersRequest() } c.InitBaseRequest(&request.BaseRequest, "clb", tcclb.APIVersion, "DescribeListeners") if c.GetCredential() == nil { return nil, errors.New("DescribeListeners require credential") } request.SetContext(ctx) response = tcclb.NewDescribeListenersResponse() err = c.Send(request, response) return } func (c *ClbClient) DescribeTaskStatus(request *tcclb.DescribeTaskStatusRequest) (response *tcclb.DescribeTaskStatusResponse, err error) { return c.DescribeTaskStatusWithContext(context.Background(), request) } func (c *ClbClient) DescribeTaskStatusWithContext(ctx context.Context, request *tcclb.DescribeTaskStatusRequest) (response *tcclb.DescribeTaskStatusResponse, err error) { if request == nil { request = tcclb.NewDescribeTaskStatusRequest() } c.InitBaseRequest(&request.BaseRequest, "clb", tcclb.APIVersion, "DescribeTaskStatus") if c.GetCredential() == nil { return nil, errors.New("DescribeTaskStatus require credential") } request.SetContext(ctx) response = tcclb.NewDescribeTaskStatusResponse() err = c.Send(request, response) return } func (c *ClbClient) ModifyDomainAttributes(request *tcclb.ModifyDomainAttributesRequest) (response *tcclb.ModifyDomainAttributesResponse, err error) { return c.ModifyDomainAttributesWithContext(context.Background(), request) } func (c *ClbClient) ModifyDomainAttributesWithContext(ctx context.Context, request *tcclb.ModifyDomainAttributesRequest) (response *tcclb.ModifyDomainAttributesResponse, err error) { if request == nil { request = tcclb.NewModifyDomainAttributesRequest() } c.InitBaseRequest(&request.BaseRequest, "clb", tcclb.APIVersion, "ModifyDomainAttributes") if c.GetCredential() == nil { return nil, errors.New("ModifyDomainAttributes require credential") } request.SetContext(ctx) response = tcclb.NewModifyDomainAttributesResponse() err = c.Send(request, response) return } func (c *ClbClient) ModifyListener(request *tcclb.ModifyListenerRequest) (response *tcclb.ModifyListenerResponse, err error) { return c.ModifyListenerWithContext(context.Background(), request) } func (c *ClbClient) ModifyListenerWithContext(ctx context.Context, request *tcclb.ModifyListenerRequest) (response *tcclb.ModifyListenerResponse, err error) { if request == nil { request = tcclb.NewModifyListenerRequest() } c.InitBaseRequest(&request.BaseRequest, "clb", tcclb.APIVersion, "ModifyListener") if c.GetCredential() == nil { return nil, errors.New("ModifyListener require credential") } request.SetContext(ctx) response = tcclb.NewModifyListenerResponse() err = c.Send(request, response) return } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-clb/tencentcloud_clb.go ================================================ package tencentcloudclb import ( "context" "errors" "fmt" "log/slog" "strings" "time" "github.com/samber/lo" tcclb "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb/v20180317" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-clb/internal" xwait "github.com/certimate-go/certimate/pkg/utils/wait" ) type DeployerConfig struct { // 腾讯云 SecretId。 SecretId string `json:"secretId"` // 腾讯云 SecretKey。 SecretKey string `json:"secretKey"` // 腾讯云接口端点。 Endpoint string `json:"endpoint,omitempty"` // 腾讯云地域。 Region string `json:"region"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 负载均衡器 ID。 // 部署资源类型为 [RESOURCE_TYPE_SSLDEPLOY]、[RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_RULEDOMAIN] 时必填。 LoadbalancerId string `json:"loadbalancerId,omitempty"` // 负载均衡监听 ID。 // 部署资源类型为 [RESOURCE_TYPE_SSLDEPLOY]、[RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER]、[RESOURCE_TYPE_RULEDOMAIN] 时必填。 ListenerId string `json:"listenerId,omitempty"` // SNI 域名或七层转发规则域名(支持泛域名)。 // 部署资源类型为 [RESOURCE_TYPE_SSLDEPLOY] 时选填;部署资源类型为 [RESOURCE_TYPE_RULEDOMAIN] 时必填。 Domain string `json:"domain,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.ClbClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ SecretId: config.SecretId, SecretKey: config.SecretKey, Endpoint: lo. If(strings.HasSuffix(config.Endpoint, "intl.tencentcloudapi.com"), "ssl.intl.tencentcloudapi.com"). // 国际站使用独立的接口端点 Else(""), }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_LOADBALANCER: if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil { return nil, err } case RESOURCE_TYPE_LISTENER: if err := d.deployToListener(ctx, upres.CertId); err != nil { return nil, err } case RESOURCE_TYPE_RULEDOMAIN: if err := d.deployToRuleDomain(ctx, upres.CertId); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error { if d.config.LoadbalancerId == "" { return errors.New("config `loadbalancerId` is required") } // 查询监听器列表 // REF: https://cloud.tencent.com/document/api/214/30686 listenerIds := make([]string, 0) describeListenersReq := tcclb.NewDescribeListenersRequest() describeListenersReq.LoadBalancerId = common.StringPtr(d.config.LoadbalancerId) describeListenersResp, err := d.sdkClient.DescribeListeners(describeListenersReq) d.logger.Debug("sdk request 'clb.DescribeListeners'", slog.Any("request", describeListenersReq), slog.Any("response", describeListenersResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'clb.DescribeListeners': %w", err) } else { if describeListenersResp.Response.Listeners != nil { for _, listener := range describeListenersResp.Response.Listeners { if listener.Protocol == nil || (*listener.Protocol != "HTTPS" && *listener.Protocol != "TCP_SSL" && *listener.Protocol != "QUIC") { continue } listenerIds = append(listenerIds, *listener.ListenerId) } } } // 遍历更新监听器证书 if len(listenerIds) == 0 { d.logger.Info("no clb listeners to deploy") } else { d.logger.Info("found https/tcpssl/quic listeners to deploy", slog.Any("listenerIds", listenerIds)) var errs []error for _, listenerId := range listenerIds { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listenerId, cloudCertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error { if d.config.LoadbalancerId == "" { return errors.New("config `loadbalancerId` is required") } if d.config.ListenerId == "" { return errors.New("config `listenerId` is required") } // 更新监听器证书 if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, d.config.ListenerId, cloudCertId); err != nil { return err } return nil } func (d *Deployer) deployToRuleDomain(ctx context.Context, cloudCertId string) error { if d.config.LoadbalancerId == "" { return errors.New("config `loadbalancerId` is required") } if d.config.ListenerId == "" { return errors.New("config `listenerId` is required") } if d.config.Domain == "" { return errors.New("config `domain` is required") } // 修改负载均衡七层监听器转发规则的域名级别属性 // REF: https://cloud.tencent.com/document/api/214/38092 modifyDomainAttributesReq := tcclb.NewModifyDomainAttributesRequest() modifyDomainAttributesReq.LoadBalancerId = common.StringPtr(d.config.LoadbalancerId) modifyDomainAttributesReq.ListenerId = common.StringPtr(d.config.ListenerId) modifyDomainAttributesReq.Domain = common.StringPtr(d.config.Domain) modifyDomainAttributesReq.Certificate = &tcclb.CertificateInput{ SSLMode: common.StringPtr("UNIDIRECTIONAL"), CertId: common.StringPtr(cloudCertId), } modifyDomainAttributesResp, err := d.sdkClient.ModifyDomainAttributes(modifyDomainAttributesReq) d.logger.Debug("sdk request 'clb.ModifyDomainAttributes'", slog.Any("request", modifyDomainAttributesReq), slog.Any("response", modifyDomainAttributesResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'clb.ModifyDomainAttributes': %w", err) } // 查询异步任务状态,等待任务状态变更 // REF: https://cloud.tencent.com/document/product/214/30683 if _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) { describeTaskStatusReq := tcclb.NewDescribeTaskStatusRequest() describeTaskStatusReq.TaskId = modifyDomainAttributesResp.Response.RequestId describeTaskStatusResp, err := d.sdkClient.DescribeTaskStatus(describeTaskStatusReq) d.logger.Debug("sdk request 'clb.DescribeTaskStatus'", slog.Any("request", describeTaskStatusReq), slog.Any("response", describeTaskStatusResp)) if err != nil { return false, fmt.Errorf("failed to execute sdk request 'clb.DescribeTaskStatus': %w", err) } switch lo.FromPtr(describeTaskStatusResp.Response.Status) { case 0: return true, nil case 1: return false, fmt.Errorf("unexpected tencentcloud task status") } d.logger.Info("waiting for tencentcloud task completion ...") return false, nil }, time.Second*5); err != nil { return err } return nil } func (d *Deployer) updateListenerCertificate(ctx context.Context, cloudLoadbalancerId, cloudListenerId, cloudCertId string) error { // 查询负载均衡的监听器列表 // REF: https://cloud.tencent.com/document/api/214/30686 describeListenersReq := tcclb.NewDescribeListenersRequest() describeListenersReq.LoadBalancerId = common.StringPtr(cloudLoadbalancerId) describeListenersReq.ListenerIds = common.StringPtrs([]string{cloudListenerId}) describeListenersResp, err := d.sdkClient.DescribeListeners(describeListenersReq) d.logger.Debug("sdk request 'clb.DescribeListeners'", slog.Any("request", describeListenersReq), slog.Any("response", describeListenersResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'clb.DescribeListeners': %w", err) } else if len(describeListenersResp.Response.Listeners) == 0 { return fmt.Errorf("could not find listener '%s'", cloudListenerId) } // 修改监听器属性 // REF: https://cloud.tencent.com/document/api/214/30681 modifyListenerReq := tcclb.NewModifyListenerRequest() modifyListenerReq.LoadBalancerId = common.StringPtr(cloudLoadbalancerId) modifyListenerReq.ListenerId = common.StringPtr(cloudListenerId) modifyListenerReq.Certificate = &tcclb.CertificateInput{CertId: common.StringPtr(cloudCertId)} if describeListenersResp.Response.Listeners[0].Certificate != nil && describeListenersResp.Response.Listeners[0].Certificate.SSLMode != nil { modifyListenerReq.Certificate.SSLMode = describeListenersResp.Response.Listeners[0].Certificate.SSLMode modifyListenerReq.Certificate.CertCaId = describeListenersResp.Response.Listeners[0].Certificate.CertCaId } else { modifyListenerReq.Certificate.SSLMode = common.StringPtr("UNIDIRECTIONAL") } modifyListenerResp, err := d.sdkClient.ModifyListener(modifyListenerReq) d.logger.Debug("sdk request 'clb.ModifyListener'", slog.Any("request", modifyListenerReq), slog.Any("response", modifyListenerResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'clb.ModifyListener': %w", err) } // 查询异步任务状态,等待任务状态变更 // REF: https://cloud.tencent.com/document/product/214/30683 if _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) { describeTaskStatusReq := tcclb.NewDescribeTaskStatusRequest() describeTaskStatusReq.TaskId = modifyListenerResp.Response.RequestId describeTaskStatusResp, err := d.sdkClient.DescribeTaskStatus(describeTaskStatusReq) d.logger.Debug("sdk request 'clb.DescribeTaskStatus'", slog.Any("request", describeTaskStatusReq), slog.Any("response", describeTaskStatusResp)) if err != nil { return false, fmt.Errorf("failed to execute sdk request 'clb.DescribeTaskStatus': %w", err) } switch lo.FromPtr(describeTaskStatusResp.Response.Status) { case 0: return true, nil case 1: return false, fmt.Errorf("unexpected tencentcloud task status") } d.logger.Info("waiting for tencentcloud task completion ...") return false, nil }, time.Second*5); err != nil { return err } return nil } func createSDKClient(secretId, secretKey, endpoint, region string) (*internal.ClbClient, error) { credential := common.NewCredential(secretId, secretKey) cpf := profile.NewClientProfile() if endpoint != "" { cpf.HttpProfile.Endpoint = endpoint } client, err := internal.NewClbClient(credential, region, cpf) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-clb/tencentcloud_clb_test.go ================================================ package tencentcloudclb_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-clb" ) var ( fInputCertPath string fInputKeyPath string fSecretId string fSecretKey string fRegion string fLoadbalancerId string fListenerId string fDomain string ) func init() { argsPrefix := "TENCENTCLOUDCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "") flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "") flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./tencentcloud_clb_test.go -args \ --TENCENTCLOUDCLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --TENCENTCLOUDCLB_INPUTKEYPATH="/path/to/your-input-key.pem" \ --TENCENTCLOUDCLB_SECRETID="your-secret-id" \ --TENCENTCLOUDCLB_SECRETKEY="your-secret-key" \ --TENCENTCLOUDCLB_REGION="ap-guangzhou" \ --TENCENTCLOUDCLB_LOADBALANCERID="your-clb-lb-id" \ --TENCENTCLOUDCLB_LISTENERID="your-clb-lbl-id" \ --TENCENTCLOUDCLB_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy_ToLoadbalancer", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SECRETID: %v", fSecretId), fmt.Sprintf("SECRETKEY: %v", fSecretKey), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ SecretId: fSecretId, SecretKey: fSecretKey, Region: fRegion, ResourceType: provider.RESOURCE_TYPE_LOADBALANCER, LoadbalancerId: fLoadbalancerId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) t.Run("Deploy_ToListener", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SECRETID: %v", fSecretId), fmt.Sprintf("SECRETKEY: %v", fSecretKey), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId), fmt.Sprintf("LISTENERID: %v", fListenerId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ SecretId: fSecretId, SecretKey: fSecretKey, Region: fRegion, ResourceType: provider.RESOURCE_TYPE_LISTENER, LoadbalancerId: fLoadbalancerId, ListenerId: fListenerId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) t.Run("Deploy_ToRuleDomain", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SECRETID: %v", fSecretId), fmt.Sprintf("SECRETKEY: %v", fSecretKey), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId), fmt.Sprintf("LISTENERID: %v", fListenerId), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ SecretId: fSecretId, SecretKey: fSecretKey, Region: fRegion, ResourceType: provider.RESOURCE_TYPE_RULEDOMAIN, LoadbalancerId: fLoadbalancerId, ListenerId: fListenerId, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-cos/internal/client.go ================================================ package internal import ( "context" "errors" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" tcssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" ) // This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/ssl/v20191205/client.go // to lightweight the vendor packages in the built binary. type SslClient struct { common.Client } func NewSslClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *SslClient, err error) { client = &SslClient{} client.Init(region). WithCredential(credential). WithProfile(clientProfile) return } func (c *SslClient) DescribeHostCosInstanceList(request *tcssl.DescribeHostCosInstanceListRequest) (response *tcssl.DescribeHostCosInstanceListResponse, err error) { return c.DescribeHostCosInstanceListWithContext(context.Background(), request) } func (c *SslClient) DescribeHostCosInstanceListWithContext(ctx context.Context, request *tcssl.DescribeHostCosInstanceListRequest) (response *tcssl.DescribeHostCosInstanceListResponse, err error) { if request == nil { request = tcssl.NewDescribeHostCosInstanceListRequest() } c.InitBaseRequest(&request.BaseRequest, "ssl", tcssl.APIVersion, "DescribeHostCosInstanceList") if c.GetCredential() == nil { return nil, errors.New("DescribeHostCosInstanceList require credential") } request.SetContext(ctx) response = tcssl.NewDescribeHostCosInstanceListResponse() err = c.Send(request, response) return } func (c *SslClient) DescribeHostDeployRecordDetail(request *tcssl.DescribeHostDeployRecordDetailRequest) (response *tcssl.DescribeHostDeployRecordDetailResponse, err error) { return c.DescribeHostDeployRecordDetailWithContext(context.Background(), request) } func (c *SslClient) DescribeHostDeployRecordDetailWithContext(ctx context.Context, request *tcssl.DescribeHostDeployRecordDetailRequest) (response *tcssl.DescribeHostDeployRecordDetailResponse, err error) { if request == nil { request = tcssl.NewDescribeHostDeployRecordDetailRequest() } c.InitBaseRequest(&request.BaseRequest, "ssl", tcssl.APIVersion, "DescribeHostDeployRecordDetail") if c.GetCredential() == nil { return nil, errors.New("DescribeHostDeployRecordDetail require credential") } request.SetContext(ctx) response = tcssl.NewDescribeHostDeployRecordDetailResponse() err = c.Send(request, response) return } func (c *SslClient) DeployCertificateInstance(request *tcssl.DeployCertificateInstanceRequest) (response *tcssl.DeployCertificateInstanceResponse, err error) { return c.DeployCertificateInstanceWithContext(context.Background(), request) } func (c *SslClient) DeployCertificateInstanceWithContext(ctx context.Context, request *tcssl.DeployCertificateInstanceRequest) (response *tcssl.DeployCertificateInstanceResponse, err error) { if request == nil { request = tcssl.NewDeployCertificateInstanceRequest() } c.InitBaseRequest(&request.BaseRequest, "ssl", tcssl.APIVersion, "DeployCertificateInstance") if c.GetCredential() == nil { return nil, errors.New("DeployCertificateInstance require credential") } request.SetContext(ctx) response = tcssl.NewDeployCertificateInstanceResponse() err = c.Send(request, response) return } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-cos/tencentcloud_cos.go ================================================ package tencentcloudcos import ( "context" "errors" "fmt" "log/slog" "time" "github.com/samber/lo" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" tcssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-cos/internal" xwait "github.com/certimate-go/certimate/pkg/utils/wait" ) type DeployerConfig struct { // 腾讯云 SecretId。 SecretId string `json:"secretId"` // 腾讯云 SecretKey。 SecretKey string `json:"secretKey"` // 腾讯云地域。 Region string `json:"region"` // 存储桶名。 Bucket string `json:"bucket"` // 自定义域名(不支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *wSDKClients sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) type wSDKClients struct { SSL *internal.SslClient } func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } clients, err := createSDKClients(config.SecretId, config.SecretKey, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ SecretId: config.SecretId, SecretKey: config.SecretKey, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: clients, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { if d.config.Bucket == "" { return nil, errors.New("config `bucket` is required") } if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 避免多次部署,否则会报错 https://github.com/certimate-go/certimate/issues/897#issuecomment-3182904098 if bind, _ := d.checkIsBind(ctx, upres.CertId); bind { d.logger.Info("ssl certificate already deployed") return &deployer.DeployResult{}, nil } // 证书部署到 COS 实例 // REF: https://cloud.tencent.com/document/api/400/91667 deployCertificateInstanceReq := tcssl.NewDeployCertificateInstanceRequest() deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId) deployCertificateInstanceReq.ResourceType = common.StringPtr("cos") deployCertificateInstanceReq.Status = common.Int64Ptr(1) deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s|%s", d.config.Region, d.config.Bucket, d.config.Domain)}) deployCertificateInstanceResp, err := d.sdkClient.SSL.DeployCertificateInstance(deployCertificateInstanceReq) d.logger.Debug("sdk request 'ssl.DeployCertificateInstance'", slog.Any("request", deployCertificateInstanceReq), slog.Any("response", deployCertificateInstanceResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'ssl.DeployCertificateInstance': %w", err) } // 获取部署任务详情,等待任务状态变更 // REF: https://cloud.tencent.com/document/api/400/91658 if _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) { describeHostDeployRecordDetailReq := tcssl.NewDescribeHostDeployRecordDetailRequest() describeHostDeployRecordDetailReq.DeployRecordId = common.StringPtr(fmt.Sprintf("%d", *deployCertificateInstanceResp.Response.DeployRecordId)) describeHostDeployRecordDetailResp, err := d.sdkClient.SSL.DescribeHostDeployRecordDetail(describeHostDeployRecordDetailReq) d.logger.Debug("sdk request 'ssl.DescribeHostDeployRecordDetail'", slog.Any("request", describeHostDeployRecordDetailReq), slog.Any("response", describeHostDeployRecordDetailResp)) if err != nil { return false, fmt.Errorf("failed to execute sdk request 'ssl.DescribeHostDeployRecordDetail': %w", err) } var pendingCount, runningCount, succeededCount, failedCount, totalCount int64 if describeHostDeployRecordDetailResp.Response.TotalCount == nil { return false, fmt.Errorf("unexpected tencentcloud deployment job status") } else { pendingCount = lo.FromPtr(describeHostDeployRecordDetailResp.Response.PendingTotalCount) runningCount = lo.FromPtr(describeHostDeployRecordDetailResp.Response.RunningTotalCount) succeededCount = lo.FromPtr(describeHostDeployRecordDetailResp.Response.SuccessTotalCount) failedCount = lo.FromPtr(describeHostDeployRecordDetailResp.Response.FailedTotalCount) totalCount = lo.FromPtr(describeHostDeployRecordDetailResp.Response.TotalCount) if succeededCount+failedCount == totalCount { if failedCount > 0 { return false, fmt.Errorf("tencentcloud deployment job failed (succeeded: %d, failed: %d, total: %d)", succeededCount, failedCount, totalCount) } return true, nil } } d.logger.Info(fmt.Sprintf("waiting for tencentcloud deployment job completion (pending: %d, running: %d, succeeded: %d, failed: %d, total: %d) ...", pendingCount, runningCount, succeededCount, failedCount, totalCount)) return false, nil }, time.Second*5); err != nil { return nil, err } return &deployer.DeployResult{}, nil } func (d *Deployer) checkIsBind(ctx context.Context, cloudCertId string) (bool, error) { // 查询证书 COS 云资源部署实例列表 // REF: https://cloud.tencent.com/document/api/400/91661 describeHostCosInstanceListLimit := 100 describeHostCosInstanceListOffset := 0 for { select { case <-ctx.Done(): return false, ctx.Err() default: } describeHostCosInstanceListReq := tcssl.NewDescribeHostCosInstanceListRequest() describeHostCosInstanceListReq.OldCertificateId = common.StringPtr(cloudCertId) describeHostCosInstanceListReq.ResourceType = common.StringPtr("cos") describeHostCosInstanceListReq.IsCache = common.Uint64Ptr(0) describeHostCosInstanceListReq.Offset = common.Int64Ptr(int64(describeHostCosInstanceListOffset)) describeHostCosInstanceListReq.Limit = common.Int64Ptr(int64(describeHostCosInstanceListLimit)) describeHostCosInstanceListResp, err := d.sdkClient.SSL.DescribeHostCosInstanceList(describeHostCosInstanceListReq) d.logger.Debug("sdk request 'ssl.DescribeHostCosInstanceList'", slog.Any("request", describeHostCosInstanceListReq), slog.Any("response", describeHostCosInstanceListResp)) if err != nil { return false, fmt.Errorf("failed to execute sdk request 'ssl.DescribeHostCosInstanceList': %w", err) } if describeHostCosInstanceListResp.Response == nil { break } for _, instance := range describeHostCosInstanceListResp.Response.InstanceList { if lo.FromPtr(instance.Bucket) != d.config.Bucket { continue } if lo.FromPtr(instance.Domain) != d.config.Domain { continue } if lo.FromPtr(instance.Status) != "ENABLED" { continue } return true, nil } if len(describeHostCosInstanceListResp.Response.InstanceList) < describeHostCosInstanceListLimit { break } describeHostCosInstanceListOffset += describeHostCosInstanceListLimit } return false, nil } func createSDKClients(secretId, secretKey, region string) (*wSDKClients, error) { credential := common.NewCredential(secretId, secretKey) client, err := internal.NewSslClient(credential, region, profile.NewClientProfile()) if err != nil { return nil, err } return &wSDKClients{ SSL: client, }, nil } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-cos/tencentcloud_cos_test.go ================================================ package tencentcloudcos_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-cos" ) var ( fInputCertPath string fInputKeyPath string fSecretId string fSecretKey string fRegion string fBucket string fDomain string ) func init() { argsPrefix := "TENCENTCLOUDCOS_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "") flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fBucket, argsPrefix+"BUCKET", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./tencentcloud_cos_test.go -args \ --TENCENTCLOUDCOS_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --TENCENTCLOUDCOS_INPUTKEYPATH="/path/to/your-input-key.pem" \ --TENCENTCLOUDCOS_SECRETID="your-secret-id" \ --TENCENTCLOUDCOS_SECRETKEY="your-secret-key" \ --TENCENTCLOUDCOS_REGION="ap-guangzhou" \ --TENCENTCLOUDCOS_BUCKET="your-cos-bucket" \ --TENCENTCLOUDCOS_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SECRETID: %v", fSecretId), fmt.Sprintf("SECRETKEY: %v", fSecretKey), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("BUCKET: %v", fBucket), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ SecretId: fSecretId, SecretKey: fSecretKey, Region: fRegion, Bucket: fBucket, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-css/consts.go ================================================ package tencentcloudcss const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/tencentcloud-css/internal/client.go ================================================ package internal import ( "context" "errors" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" tclive "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/live/v20180801" ) // This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/live/v20180801/client.go // to lightweight the vendor packages in the built binary. type LiveClient struct { common.Client } func NewLiveClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *LiveClient, err error) { client = &LiveClient{} client.Init(region). WithCredential(credential). WithProfile(clientProfile) return } func (c *LiveClient) DescribeLiveDomains(request *tclive.DescribeLiveDomainsRequest) (response *tclive.DescribeLiveDomainsResponse, err error) { return c.DescribeLiveDomainsWithContext(context.Background(), request) } func (c *LiveClient) DescribeLiveDomainsWithContext(ctx context.Context, request *tclive.DescribeLiveDomainsRequest) (response *tclive.DescribeLiveDomainsResponse, err error) { if request == nil { request = tclive.NewDescribeLiveDomainsRequest() } c.InitBaseRequest(&request.BaseRequest, "live", tclive.APIVersion, "DescribeLiveDomains") if c.GetCredential() == nil { return nil, errors.New("DescribeLiveDomains require credential") } request.SetContext(ctx) response = tclive.NewDescribeLiveDomainsResponse() err = c.Send(request, response) return } func (c *LiveClient) ModifyLiveDomainCertBindings(request *tclive.ModifyLiveDomainCertBindingsRequest) (response *tclive.ModifyLiveDomainCertBindingsResponse, err error) { return c.ModifyLiveDomainCertBindingsWithContext(context.Background(), request) } func (c *LiveClient) ModifyLiveDomainCertBindingsWithContext(ctx context.Context, request *tclive.ModifyLiveDomainCertBindingsRequest) (response *tclive.ModifyLiveDomainCertBindingsResponse, err error) { if request == nil { request = tclive.NewModifyLiveDomainCertBindingsRequest() } c.InitBaseRequest(&request.BaseRequest, "live", tclive.APIVersion, "ModifyLiveDomainCertBindings") if c.GetCredential() == nil { return nil, errors.New("ModifyLiveDomainCertBindings require credential") } request.SetContext(ctx) response = tclive.NewModifyLiveDomainCertBindingsResponse() err = c.Send(request, response) return } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-css/tencentcloud_css.go ================================================ package tencentcloudcss import ( "context" "errors" "fmt" "log/slog" "strings" "github.com/samber/lo" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" tclive "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/live/v20180801" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-css/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type DeployerConfig struct { // 腾讯云 SecretId。 SecretId string `json:"secretId"` // 腾讯云 SecretKey。 SecretKey string `json:"secretKey"` // 腾讯云接口端点。 Endpoint string `json:"endpoint,omitempty"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 直播播放域名(不支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.LiveClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ SecretId: config.SecretId, SecretKey: config.SecretKey, Endpoint: lo. If(strings.HasSuffix(config.Endpoint, "intl.tencentcloudapi.com"), "ssl.intl.tencentcloudapi.com"). // 国际站使用独立的接口端点 Else(""), }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 批量绑定证书对应的播放域名 // REF: https://cloud.tencent.com/document/api/267/78655 modifyLiveDomainCertBindingsReq := tclive.NewModifyLiveDomainCertBindingsRequest() modifyLiveDomainCertBindingsReq.DomainInfos = lo.Map(domains, func(domain string, _ int) *tclive.LiveCertDomainInfo { return &tclive.LiveCertDomainInfo{ DomainName: common.StringPtr(domain), Status: common.Int64Ptr(1), } }) modifyLiveDomainCertBindingsReq.CloudCertId = common.StringPtr(upres.CertId) modifyLiveDomainCertBindingsResp, err := d.sdkClient.ModifyLiveDomainCertBindings(modifyLiveDomainCertBindingsReq) d.logger.Debug("sdk request 'live.ModifyLiveDomainCertBindings'", slog.Any("request", modifyLiveDomainCertBindingsReq), slog.Any("response", modifyLiveDomainCertBindingsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'live.ModifyLiveDomainCertBindings': %w", err) } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 查询域名列表 // REF: https://cloud.tencent.com/document/api/267/33856 describeLiveDomainsPageNum := 1 describeLiveDomainsPageSize := 100 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } describeLiveDomainsReq := tclive.NewDescribeLiveDomainsRequest() describeLiveDomainsReq.DomainStatus = common.Uint64Ptr(1) describeLiveDomainsReq.DomainType = common.Uint64Ptr(1) describeLiveDomainsReq.PageNum = common.Uint64Ptr(uint64(describeLiveDomainsPageNum)) describeLiveDomainsReq.PageSize = common.Uint64Ptr(uint64(describeLiveDomainsPageSize)) describeLiveDomainsResp, err := d.sdkClient.DescribeLiveDomains(describeLiveDomainsReq) d.logger.Debug("sdk request 'live.DescribeLiveDomains'", slog.Any("request", describeLiveDomainsReq), slog.Any("response", describeLiveDomainsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'live.DescribeLiveDomains': %w", err) } if describeLiveDomainsResp.Response == nil { break } for _, domainItem := range describeLiveDomainsResp.Response.DomainList { domains = append(domains, *domainItem.Name) } if len(describeLiveDomainsResp.Response.DomainList) < describeLiveDomainsPageSize { break } describeLiveDomainsPageNum++ } return domains, nil } func createSDKClient(secretId, secretKey, endpoint string) (*internal.LiveClient, error) { credential := common.NewCredential(secretId, secretKey) cpf := profile.NewClientProfile() if endpoint != "" { cpf.HttpProfile.Endpoint = endpoint } client, err := internal.NewLiveClient(credential, "", cpf) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-css/tencentcloud_css_test.go ================================================ package tencentcloudcss_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-css" ) var ( fInputCertPath string fInputKeyPath string fSecretId string fSecretKey string fDomain string ) func init() { argsPrefix := "TENCENTCLOUDCSS_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "") flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./tencentcloud_css_test.go -args \ --TENCENTCLOUDCSS_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --TENCENTCLOUDCSS_INPUTKEYPATH="/path/to/your-input-key.pem" \ --TENCENTCLOUDCSS_SECRETID="your-secret-id" \ --TENCENTCLOUDCSS_SECRETKEY="your-secret-key" \ --TENCENTCLOUDCSS_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SECRETID: %v", fSecretId), fmt.Sprintf("SECRETKEY: %v", fSecretKey), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ SecretId: fSecretId, SecretKey: fSecretKey, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-ecdn/consts.go ================================================ package tencentcloudecdn const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/tencentcloud-ecdn/internal/client.go ================================================ package internal import ( "context" "errors" tccdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" ) // This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/cdn/v20180606/client.go // to lightweight the vendor packages in the built binary. type CdnClient struct { common.Client } func NewCdnClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *CdnClient, err error) { client = &CdnClient{} client.Init(region). WithCredential(credential). WithProfile(clientProfile) return } func (c *CdnClient) DescribeCertDomains(request *tccdn.DescribeCertDomainsRequest) (response *tccdn.DescribeCertDomainsResponse, err error) { return c.DescribeCertDomainsWithContext(context.Background(), request) } func (c *CdnClient) DescribeCertDomainsWithContext(ctx context.Context, request *tccdn.DescribeCertDomainsRequest) (response *tccdn.DescribeCertDomainsResponse, err error) { if request == nil { request = tccdn.NewDescribeCertDomainsRequest() } c.InitBaseRequest(&request.BaseRequest, "cdn", tccdn.APIVersion, "DescribeCertDomains") if c.GetCredential() == nil { return nil, errors.New("DescribeCertDomains require credential") } request.SetContext(ctx) response = tccdn.NewDescribeCertDomainsResponse() err = c.Send(request, response) return } func (c *CdnClient) DescribeDomains(request *tccdn.DescribeDomainsRequest) (response *tccdn.DescribeDomainsResponse, err error) { return c.DescribeDomainsWithContext(context.Background(), request) } func (c *CdnClient) DescribeDomainsWithContext(ctx context.Context, request *tccdn.DescribeDomainsRequest) (response *tccdn.DescribeDomainsResponse, err error) { if request == nil { request = tccdn.NewDescribeDomainsRequest() } c.InitBaseRequest(&request.BaseRequest, "cdn", tccdn.APIVersion, "DescribeDomains") if c.GetCredential() == nil { return nil, errors.New("DescribeDomains require credential") } request.SetContext(ctx) response = tccdn.NewDescribeDomainsResponse() err = c.Send(request, response) return } func (c *CdnClient) DescribeDomainsConfig(request *tccdn.DescribeDomainsConfigRequest) (response *tccdn.DescribeDomainsConfigResponse, err error) { return c.DescribeDomainsConfigWithContext(context.Background(), request) } func (c *CdnClient) DescribeDomainsConfigWithContext(ctx context.Context, request *tccdn.DescribeDomainsConfigRequest) (response *tccdn.DescribeDomainsConfigResponse, err error) { if request == nil { request = tccdn.NewDescribeDomainsConfigRequest() } c.InitBaseRequest(&request.BaseRequest, "cdn", tccdn.APIVersion, "DescribeDomainsConfig") if c.GetCredential() == nil { return nil, errors.New("DescribeDomainsConfig require credential") } request.SetContext(ctx) response = tccdn.NewDescribeDomainsConfigResponse() err = c.Send(request, response) return } func (c *CdnClient) UpdateDomainConfig(request *tccdn.UpdateDomainConfigRequest) (response *tccdn.UpdateDomainConfigResponse, err error) { return c.UpdateDomainConfigWithContext(context.Background(), request) } func (c *CdnClient) UpdateDomainConfigWithContext(ctx context.Context, request *tccdn.UpdateDomainConfigRequest) (response *tccdn.UpdateDomainConfigResponse, err error) { if request == nil { request = tccdn.NewUpdateDomainConfigRequest() } c.InitBaseRequest(&request.BaseRequest, "cdn", tccdn.APIVersion, "UpdateDomainConfig") if c.GetCredential() == nil { return nil, errors.New("UpdateDomainConfig require credential") } request.SetContext(ctx) response = tccdn.NewUpdateDomainConfigResponse() err = c.Send(request, response) return } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go ================================================ package tencentcloudecdn import ( "context" "errors" "fmt" "log/slog" "strings" "github.com/samber/lo" tccdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-ecdn/internal" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 腾讯云 SecretId。 SecretId string `json:"secretId"` // 腾讯云 SecretKey。 SecretKey string `json:"secretKey"` // 腾讯云接口端点。 Endpoint string `json:"endpoint,omitempty"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.CdnClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ SecretId: config.SecretId, SecretKey: config.SecretKey, Endpoint: lo. If(strings.HasSuffix(config.Endpoint, "intl.tencentcloudapi.com"), "ssl.intl.tencentcloudapi.com"). // 国际站使用独立的接口端点 Else(""), }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的 ECDN 实例 domains := make([]string, 0) switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getMatchedDomainsByWildcard(ctx, d.config.Domain) if err != nil { return nil, err } domains = domainCandidates } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { domainCandidates, err := d.getMatchedDomainsByCertId(ctx, upres.CertId) if err != nil { return nil, err } domains = domainCandidates } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no ecdn domains to deploy") } else { d.logger.Info("found ecdn domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getMatchedDomainsByWildcard(ctx context.Context, wildcardDomain string) ([]string, error) { domains := make([]string, 0) // 查询域名基本信息,获取匹配的域名 // REF: https://cloud.tencent.com/document/api/228/41118 describeDomainsOffset := 0 describeDomainsLimit := 100 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } describeDomainsReq := tccdn.NewDescribeDomainsRequest() describeDomainsReq.Filters = []*tccdn.DomainFilter{ { Name: common.StringPtr("domain"), Value: common.StringPtrs([]string{strings.TrimPrefix(wildcardDomain, "*.")}), Fuzzy: common.BoolPtr(true), }, } describeDomainsReq.Offset = common.Int64Ptr(int64(describeDomainsOffset)) describeDomainsReq.Limit = common.Int64Ptr(int64(describeDomainsLimit)) describeDomainsResp, err := d.sdkClient.DescribeDomains(describeDomainsReq) d.logger.Debug("sdk request 'cdn.DescribeDomains'", slog.Any("request", describeDomainsReq), slog.Any("response", describeDomainsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.DescribeDomains': %w", err) } if describeDomainsResp.Response == nil { break } for _, domainItem := range describeDomainsResp.Response.Domains { if lo.FromPtr(domainItem.Product) == "ecdn" && xcerthostname.IsMatch(wildcardDomain, lo.FromPtr(domainItem.Domain)) { domains = append(domains, *domainItem.Domain) } } if len(describeDomainsResp.Response.Domains) < describeDomainsLimit { break } describeDomainsOffset += describeDomainsLimit } return domains, nil } func (d *Deployer) getMatchedDomainsByCertId(ctx context.Context, cloudCertId string) ([]string, error) { // 获取证书中的可用域名 // REF: https://cloud.tencent.com/document/api/228/42491 describeCertDomainsReq := tccdn.NewDescribeCertDomainsRequest() describeCertDomainsReq.CertId = common.StringPtr(cloudCertId) describeCertDomainsReq.Product = common.StringPtr("ecdn") describeCertDomainsResp, err := d.sdkClient.DescribeCertDomains(describeCertDomainsReq) d.logger.Debug("sdk request 'cdn.DescribeCertDomains'", slog.Any("request", describeCertDomainsReq), slog.Any("response", describeCertDomainsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.DescribeCertDomains': %w", err) } domains := make([]string, 0) if describeCertDomainsResp.Response.Domains != nil { for _, domain := range describeCertDomainsResp.Response.Domains { domains = append(domains, *domain) } } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error { // 查询域名详细配置 // REF: https://cloud.tencent.com/document/api/228/41117 describeDomainsConfigReq := tccdn.NewDescribeDomainsConfigRequest() describeDomainsConfigReq.Filters = []*tccdn.DomainFilter{ { Name: common.StringPtr("domain"), Value: common.StringPtrs([]string{domain}), }, } describeDomainsConfigReq.Offset = common.Int64Ptr(0) describeDomainsConfigReq.Limit = common.Int64Ptr(1) describeDomainsConfigResp, err := d.sdkClient.DescribeDomainsConfig(describeDomainsConfigReq) d.logger.Debug("sdk request 'cdn.DescribeDomainsConfig'", slog.Any("request", describeDomainsConfigReq), slog.Any("response", describeDomainsConfigResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'cdn.DescribeDomainsConfig': %w", err) } else if len(describeDomainsConfigResp.Response.Domains) == 0 { return fmt.Errorf("could not find domain '%s'", domain) } domainConfig := describeDomainsConfigResp.Response.Domains[0] if domainConfig.Https != nil && domainConfig.Https.CertInfo != nil && domainConfig.Https.CertInfo.CertId != nil && *domainConfig.Https.CertInfo.CertId == cloudCertId { // 已部署过此域名,跳过 return nil } // 更新加速域名配置 // REF: https://cloud.tencent.com/document/api/228/41116 updateDomainConfigReq := tccdn.NewUpdateDomainConfigRequest() updateDomainConfigReq.Domain = common.StringPtr(domain) updateDomainConfigReq.Https = domainConfig.Https if updateDomainConfigReq.Https == nil { updateDomainConfigReq.Https = &tccdn.Https{ Switch: common.StringPtr("on"), } } else { updateDomainConfigReq.Https.SslStatus = nil } updateDomainConfigReq.Https.CertInfo = &tccdn.ServerCert{ CertId: common.StringPtr(cloudCertId), } updateDomainConfigResp, err := d.sdkClient.UpdateDomainConfig(updateDomainConfigReq) d.logger.Debug("sdk request 'cdn.UpdateDomainConfig'", slog.Any("request", updateDomainConfigReq), slog.Any("response", updateDomainConfigResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'cdn.UpdateDomainConfig': %w", err) } return nil } func createSDKClient(secretId, secretKey, endpoint string) (*internal.CdnClient, error) { credential := common.NewCredential(secretId, secretKey) cpf := profile.NewClientProfile() if endpoint != "" { cpf.HttpProfile.Endpoint = endpoint } client, err := internal.NewCdnClient(credential, "", cpf) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn_test.go ================================================ package tencentcloudecdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-ecdn" ) var ( fInputCertPath string fInputKeyPath string fSecretId string fSecretKey string fDomain string ) func init() { argsPrefix := "TENCENTCLOUDECDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "") flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./tencentcloud_ecdn_test.go -args \ --TENCENTCLOUDECDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --TENCENTCLOUDECDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --TENCENTCLOUDECDN_SECRETID="your-secret-id" \ --TENCENTCLOUDECDN_SECRETKEY="your-secret-key" \ --TENCENTCLOUDECDN_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SECRETID: %v", fSecretId), fmt.Sprintf("SECRETKEY: %v", fSecretKey), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ SecretId: fSecretId, SecretKey: fSecretKey, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-eo/consts.go ================================================ package tencentcloudeo const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/tencentcloud-eo/internal/client.go ================================================ package internal import ( "context" "errors" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" tcteo "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901" ) // This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/teo/v20220901/client.go // to lightweight the vendor packages in the built binary. type TeoClient struct { common.Client } func NewTeoClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *TeoClient, err error) { client = &TeoClient{} client.Init(region). WithCredential(credential). WithProfile(clientProfile) return } func (c *TeoClient) DescribeAccelerationDomains(request *tcteo.DescribeAccelerationDomainsRequest) (response *tcteo.DescribeAccelerationDomainsResponse, err error) { return c.DescribeAccelerationDomainsWithContext(context.Background(), request) } func (c *TeoClient) DescribeAccelerationDomainsWithContext(ctx context.Context, request *tcteo.DescribeAccelerationDomainsRequest) (response *tcteo.DescribeAccelerationDomainsResponse, err error) { if request == nil { request = tcteo.NewDescribeAccelerationDomainsRequest() } c.InitBaseRequest(&request.BaseRequest, "teo", tcteo.APIVersion, "DescribeAccelerationDomains") if c.GetCredential() == nil { return nil, errors.New("DescribeAccelerationDomains require credential") } request.SetContext(ctx) response = tcteo.NewDescribeAccelerationDomainsResponse() err = c.Send(request, response) return } func (c *TeoClient) ModifyHostsCertificate(request *tcteo.ModifyHostsCertificateRequest) (response *tcteo.ModifyHostsCertificateResponse, err error) { return c.ModifyHostsCertificateWithContext(context.Background(), request) } func (c *TeoClient) ModifyHostsCertificateWithContext(ctx context.Context, request *tcteo.ModifyHostsCertificateRequest) (response *tcteo.ModifyHostsCertificateResponse, err error) { if request == nil { request = tcteo.NewModifyHostsCertificateRequest() } c.InitBaseRequest(&request.BaseRequest, "teo", tcteo.APIVersion, "ModifyHostsCertificate") if c.GetCredential() == nil { return nil, errors.New("ModifyHostsCertificate require credential") } request.SetContext(ctx) response = tcteo.NewModifyHostsCertificateResponse() err = c.Send(request, response) return } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-eo/tencentcloud_eo.go ================================================ package tencentcloudeo import ( "context" "crypto/x509" "errors" "fmt" "log/slog" "strings" "time" "github.com/samber/lo" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" tcteo "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-eo/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" xcertkey "github.com/certimate-go/certimate/pkg/utils/cert/key" ) type DeployerConfig struct { // 腾讯云 SecretId。 SecretId string `json:"secretId"` // 腾讯云 SecretKey。 SecretKey string `json:"secretKey"` // 腾讯云接口端点。 Endpoint string `json:"endpoint,omitempty"` // 站点 ID。 ZoneId string `json:"zoneId"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名列表(支持泛域名)。 Domains []string `json:"domains"` // 是否启用多证书模式。 EnableMultipleSSL bool `json:"enableMultipleSSL,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.TeoClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ SecretId: config.SecretId, SecretKey: config.SecretKey, Endpoint: lo. If(strings.HasSuffix(config.Endpoint, "intl.tencentcloudapi.com"), "ssl.intl.tencentcloudapi.com"). // 国际站使用独立的接口端点 Else(""), }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { if d.config.ZoneId == "" { return nil, errors.New("config `zoneId` is required") } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取全部可部署的域名信息 domainsInZone, err := d.getAllDomainsInZone(ctx, d.config.ZoneId) if err != nil { return nil, err } // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if len(d.config.Domains) == 0 { return nil, errors.New("config `domains` is required") } domains = d.config.Domains } case DOMAIN_MATCH_PATTERN_WILDCARD: { if len(d.config.Domains) == 0 { return nil, errors.New("config `domains` is required") } domainCandidates := lo.Map(domainsInZone, func(domainInfo *tcteo.AccelerationDomain, _ int) string { return lo.FromPtr(domainInfo.DomainName) }) domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { for _, configDomain := range d.config.Domains { if xcerthostname.IsMatch(configDomain, domain) { return true } } return false }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by wildcard") } } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates := lo.Map(domainsInZone, func(domainInfo *tcteo.AccelerationDomain, _ int) string { return lo.FromPtr(domainInfo.DomainName) }) domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 跳过已部署过的域名 domains = lo.Filter(domains, func(domain string, _ int) bool { var deployed bool domainInfo, _ := lo.Find(domainsInZone, func(domainInfo *tcteo.AccelerationDomain) bool { return domain == lo.FromPtr(domainInfo.DomainName) }) if domainInfo != nil && domainInfo.Certificate != nil { deployed = lo.ContainsBy(domainInfo.Certificate.List, func(certInfo *tcteo.CertificateInfo) bool { return upres.CertId == lo.FromPtr(certInfo.CertId) }) } return !deployed }) // 批量更新域名证书 if len(domains) == 0 { d.logger.Info("no edgeone domains to deploy") } else { d.logger.Info("found edgeone domains to deploy", slog.Any("domains", domains)) // 配置域名证书 // REF: https://cloud.tencent.com/document/api/1552/80764 modifyHostsCertificateReqs := make([]*tcteo.ModifyHostsCertificateRequest, 0) if d.config.EnableMultipleSSL { const algRSA = "RSA" const algECC = "ECC" privkey, err := xcert.ParsePrivateKeyFromPEM(privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to parse private key: %w", err) } privkeyAlg, _, _ := xcertkey.GetPrivateKeyAlgorithm(privkey) privkeyAlgStr := "" switch privkeyAlg { case x509.RSA: privkeyAlgStr = algRSA case x509.ECDSA: privkeyAlgStr = algECC } for _, domain := range domains { modifyHostsCertificateReq := tcteo.NewModifyHostsCertificateRequest() modifyHostsCertificateReq.ZoneId = common.StringPtr(d.config.ZoneId) modifyHostsCertificateReq.Mode = common.StringPtr("sslcert") modifyHostsCertificateReq.Hosts = common.StringPtrs([]string{domain}) modifyHostsCertificateReq.ServerCertInfo = []*tcteo.ServerCertInfo{{CertId: common.StringPtr(upres.CertId)}} domainInfo, _ := lo.Find(domainsInZone, func(domainInfo *tcteo.AccelerationDomain) bool { return domain == lo.FromPtr(domainInfo.DomainName) }) if domainInfo != nil && domainInfo.Certificate != nil { for _, certInfo := range domainInfo.Certificate.List { if lo.FromPtr(certInfo.CertId) == upres.CertId { continue } if strings.Split(lo.FromPtr(certInfo.SignAlgo), " ")[0] == privkeyAlgStr { continue } certExpireTime, _ := time.Parse("2006-01-02T15:04:05Z", lo.FromPtr(certInfo.ExpireTime)) if certExpireTime.Before(time.Now()) { continue } modifyHostsCertificateReq.ServerCertInfo = append(modifyHostsCertificateReq.ServerCertInfo, &tcteo.ServerCertInfo{CertId: certInfo.CertId}) } } modifyHostsCertificateReqs = append(modifyHostsCertificateReqs, modifyHostsCertificateReq) } } else { modifyHostsCertificateReq := tcteo.NewModifyHostsCertificateRequest() modifyHostsCertificateReq.ZoneId = common.StringPtr(d.config.ZoneId) modifyHostsCertificateReq.Mode = common.StringPtr("sslcert") modifyHostsCertificateReq.Hosts = common.StringPtrs(domains) modifyHostsCertificateReq.ServerCertInfo = []*tcteo.ServerCertInfo{{CertId: common.StringPtr(upres.CertId)}} modifyHostsCertificateReqs = append(modifyHostsCertificateReqs, modifyHostsCertificateReq) } var errs []error for _, modifyHostsCertificateReq := range modifyHostsCertificateReqs { select { case <-ctx.Done(): return nil, ctx.Err() default: modifyHostsCertificateResp, err := d.sdkClient.ModifyHostsCertificate(modifyHostsCertificateReq) d.logger.Debug("sdk request 'teo.ModifyHostsCertificate'", slog.Any("request", modifyHostsCertificateReq), slog.Any("response", modifyHostsCertificateResp)) if err != nil { err = fmt.Errorf("failed to execute sdk request 'teo.ModifyHostsCertificate': %w", err) errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomainsInZone(ctx context.Context, zoneId string) ([]*tcteo.AccelerationDomain, error) { var domainsInZone []*tcteo.AccelerationDomain const pageSize = 200 for offset := 0; ; offset += pageSize { select { case <-ctx.Done(): return nil, ctx.Err() default: } // 查询加速域名列表 // REF: https://cloud.tencent.com/document/api/1552/86336 describeAccelerationDomainsReq := tcteo.NewDescribeAccelerationDomainsRequest() describeAccelerationDomainsReq.Limit = common.Int64Ptr(pageSize) describeAccelerationDomainsReq.Offset = common.Int64Ptr(int64(offset)) describeAccelerationDomainsReq.ZoneId = common.StringPtr(zoneId) describeAccelerationDomainsResp, err := d.sdkClient.DescribeAccelerationDomains(describeAccelerationDomainsReq) d.logger.Debug("sdk request 'teo.DescribeAccelerationDomains'", slog.Any("request", describeAccelerationDomainsReq), slog.Any("response", describeAccelerationDomainsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'teo.DescribeAccelerationDomains': %w", err) } ignoredStatuses := []string{"offline", "forbidden", "init"} for _, domainItem := range describeAccelerationDomainsResp.Response.AccelerationDomains { if lo.Contains(ignoredStatuses, lo.FromPtr(domainItem.DomainStatus)) { continue } domainsInZone = append(domainsInZone, domainItem) } if len(describeAccelerationDomainsResp.Response.AccelerationDomains) < pageSize { break } } return domainsInZone, nil } func createSDKClient(secretId, secretKey, endpoint string) (*internal.TeoClient, error) { credential := common.NewCredential(secretId, secretKey) cpf := profile.NewClientProfile() if endpoint != "" { cpf.HttpProfile.Endpoint = endpoint } client, err := internal.NewTeoClient(credential, "", cpf) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-eo/tencentcloud_eo_test.go ================================================ package tencentcloudeo_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-eo" ) var ( fInputCertPath string fInputKeyPath string fSecretId string fSecretKey string fZoneId string fDomains string ) func init() { argsPrefix := "TENCENTCLOUDEO_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "") flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") flag.StringVar(&fZoneId, argsPrefix+"ZONEID", "", "") flag.StringVar(&fDomains, argsPrefix+"DOMAINS", "", "") } /* Shell command to run this test: go test -v ./tencentcloud_eo_test.go -args \ --TENCENTCLOUDEO_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --TENCENTCLOUDEO_INPUTKEYPATH="/path/to/your-input-key.pem" \ --TENCENTCLOUDEO_SECRETID="your-secret-id" \ --TENCENTCLOUDEO_SECRETKEY="your-secret-key" \ --TENCENTCLOUDEO_ZONEID="your-zone-id" \ --TENCENTCLOUDEO_DOMAINS="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SECRETID: %v", fSecretId), fmt.Sprintf("SECRETKEY: %v", fSecretKey), fmt.Sprintf("ZONEID: %v", fZoneId), fmt.Sprintf("DOMAINS: %v", fDomains), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ SecretId: fSecretId, SecretKey: fSecretKey, ZoneId: fZoneId, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domains: strings.Split(fDomains, ";"), EnableMultipleSSL: true, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-gaap/consts.go ================================================ package tencentcloudgaap const ( // 资源类型:部署到指定监听器。 RESOURCE_TYPE_LISTENER = "listener" ) ================================================ FILE: pkg/core/deployer/providers/tencentcloud-gaap/internal/client.go ================================================ package internal import ( "context" "errors" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" tcgaap "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap/v20180529" ) // This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/gaap/v20180529/client.go // to lightweight the vendor packages in the built binary. type GaapClient struct { common.Client } func NewGaapClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *GaapClient, err error) { client = &GaapClient{} client.Init(region). WithCredential(credential). WithProfile(clientProfile) return } func (c *GaapClient) DescribeHTTPSListeners(request *tcgaap.DescribeHTTPSListenersRequest) (response *tcgaap.DescribeHTTPSListenersResponse, err error) { return c.DescribeHTTPSListenersWithContext(context.Background(), request) } func (c *GaapClient) DescribeHTTPSListenersWithContext(ctx context.Context, request *tcgaap.DescribeHTTPSListenersRequest) (response *tcgaap.DescribeHTTPSListenersResponse, err error) { if request == nil { request = tcgaap.NewDescribeHTTPSListenersRequest() } if c.GetCredential() == nil { return nil, errors.New("DescribeHTTPSListeners require credential") } request.SetContext(ctx) response = tcgaap.NewDescribeHTTPSListenersResponse() err = c.Send(request, response) return } func (c *GaapClient) ModifyHTTPSListenerAttribute(request *tcgaap.ModifyHTTPSListenerAttributeRequest) (response *tcgaap.ModifyHTTPSListenerAttributeResponse, err error) { return c.ModifyHTTPSListenerAttributeWithContext(context.Background(), request) } func (c *GaapClient) ModifyHTTPSListenerAttributeWithContext(ctx context.Context, request *tcgaap.ModifyHTTPSListenerAttributeRequest) (response *tcgaap.ModifyHTTPSListenerAttributeResponse, err error) { if request == nil { request = tcgaap.NewModifyHTTPSListenerAttributeRequest() } if c.GetCredential() == nil { return nil, errors.New("ModifyHTTPSListenerAttribute require credential") } request.SetContext(ctx) response = tcgaap.NewModifyHTTPSListenerAttributeResponse() err = c.Send(request, response) return } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-gaap/tencentcloud_gaap.go ================================================ package tencentcloudgaap import ( "context" "errors" "fmt" "log/slog" "strings" "github.com/samber/lo" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" tcgaap "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap/v20180529" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-gaap/internal" ) type DeployerConfig struct { // 腾讯云 SecretId。 SecretId string `json:"secretId"` // 腾讯云 SecretKey。 SecretKey string `json:"secretKey"` // 腾讯云接口端点。 Endpoint string `json:"endpoint,omitempty"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 通道 ID。 // 选填。 ProxyId string `json:"proxyId,omitempty"` // 负载均衡监听 ID。 // 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。 ListenerId string `json:"listenerId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.GaapClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClients(config.SecretId, config.SecretKey, config.Endpoint) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ SecretId: config.SecretId, SecretKey: config.SecretKey, Endpoint: lo. If(strings.HasSuffix(config.Endpoint, "intl.tencentcloudapi.com"), "ssl.intl.tencentcloudapi.com"). // 国际站使用独立的接口端点 Else(""), }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_LISTENER: if err := d.deployToListener(ctx, upres.CertId); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error { if d.config.ListenerId == "" { return errors.New("config `listenerId` is required") } // 更新监听器证书 if err := d.updateHttpsListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil { return err } return nil } func (d *Deployer) updateHttpsListenerCertificate(ctx context.Context, cloudListenerId, cloudCertId string) error { // 查询 HTTPS 监听器信息 // REF: https://cloud.tencent.com/document/api/608/37001 describeHTTPSListenersReq := tcgaap.NewDescribeHTTPSListenersRequest() describeHTTPSListenersReq.ListenerId = common.StringPtr(cloudListenerId) describeHTTPSListenersReq.Offset = common.Uint64Ptr(0) describeHTTPSListenersReq.Limit = common.Uint64Ptr(1) describeHTTPSListenersResp, err := d.sdkClient.DescribeHTTPSListeners(describeHTTPSListenersReq) d.logger.Debug("sdk request 'gaap.DescribeHTTPSListeners'", slog.Any("request", describeHTTPSListenersReq), slog.Any("response", describeHTTPSListenersResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'gaap.DescribeHTTPSListeners': %w", err) } else if len(describeHTTPSListenersResp.Response.ListenerSet) == 0 { return fmt.Errorf("could not find listener '%s'", cloudListenerId) } // 修改 HTTPS 监听器配置 // REF: https://cloud.tencent.com/document/api/608/36996 modifyHTTPSListenerAttributeReq := tcgaap.NewModifyHTTPSListenerAttributeRequest() modifyHTTPSListenerAttributeReq.ProxyId = lo.EmptyableToPtr(d.config.ProxyId) modifyHTTPSListenerAttributeReq.ListenerId = common.StringPtr(cloudListenerId) modifyHTTPSListenerAttributeReq.CertificateId = common.StringPtr(cloudCertId) modifyHTTPSListenerAttributeResp, err := d.sdkClient.ModifyHTTPSListenerAttribute(modifyHTTPSListenerAttributeReq) d.logger.Debug("sdk request 'gaap.ModifyHTTPSListenerAttribute'", slog.Any("request", modifyHTTPSListenerAttributeReq), slog.Any("response", modifyHTTPSListenerAttributeResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'gaap.ModifyHTTPSListenerAttribute': %w", err) } return nil } func createSDKClients(secretId, secretKey, endpoint string) (*internal.GaapClient, error) { credential := common.NewCredential(secretId, secretKey) cpf := profile.NewClientProfile() if endpoint != "" { cpf.HttpProfile.Endpoint = endpoint } client, err := internal.NewGaapClient(credential, "", cpf) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-gaap/tencentcloud_gaap_test.go ================================================ package tencentcloudgaap_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-gaap" ) var ( fInputCertPath string fInputKeyPath string fSecretId string fSecretKey string fProxyId string fListenerId string ) func init() { argsPrefix := "TENCENTCLOUDCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "") flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") flag.StringVar(&fProxyId, argsPrefix+"PROXYID", "", "") flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "") } /* Shell command to run this test: go test -v ./tencentcloud_gaap_test.go -args \ --TENCENTCLOUDGAAP_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --TENCENTCLOUDGAAP_INPUTKEYPATH="/path/to/your-input-key.pem" \ --TENCENTCLOUDGAAP_SECRETID="your-secret-id" \ --TENCENTCLOUDGAAP_SECRETKEY="your-secret-key" \ --TENCENTCLOUDGAAP_PROXYID="your-gaap-group-id" \ --TENCENTCLOUDGAAP_LISTENERID="your-clb-listener-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy_ToListener", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SECRETID: %v", fSecretId), fmt.Sprintf("SECRETKEY: %v", fSecretKey), fmt.Sprintf("PROXYID: %v", fProxyId), fmt.Sprintf("LISTENERID: %v", fListenerId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ SecretId: fSecretId, SecretKey: fSecretKey, ResourceType: provider.RESOURCE_TYPE_LISTENER, ProxyId: fProxyId, ListenerId: fListenerId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-scf/consts.go ================================================ package tencentcloudscf const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/tencentcloud-scf/internal/client.go ================================================ package internal import ( "context" "errors" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" tcscf "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/scf/v20180416" ) // This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/scf/v20180416/client.go // to lightweight the vendor packages in the built binary. type ScfClient struct { common.Client } func NewScfClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *ScfClient, err error) { client = &ScfClient{} client.Init(region). WithCredential(credential). WithProfile(clientProfile) return } func (c *ScfClient) GetCustomDomain(request *tcscf.GetCustomDomainRequest) (response *tcscf.GetCustomDomainResponse, err error) { return c.GetCustomDomainWithContext(context.Background(), request) } func (c *ScfClient) GetCustomDomainWithContext(ctx context.Context, request *tcscf.GetCustomDomainRequest) (response *tcscf.GetCustomDomainResponse, err error) { if request == nil { request = tcscf.NewGetCustomDomainRequest() } c.InitBaseRequest(&request.BaseRequest, "scf", tcscf.APIVersion, "GetCustomDomain") if c.GetCredential() == nil { return nil, errors.New("GetCustomDomain require credential") } request.SetContext(ctx) response = tcscf.NewGetCustomDomainResponse() err = c.Send(request, response) return } func (c *ScfClient) ListCustomDomains(request *tcscf.ListCustomDomainsRequest) (response *tcscf.ListCustomDomainsResponse, err error) { return c.ListCustomDomainsWithContext(context.Background(), request) } func (c *ScfClient) ListCustomDomainsWithContext(ctx context.Context, request *tcscf.ListCustomDomainsRequest) (response *tcscf.ListCustomDomainsResponse, err error) { if request == nil { request = tcscf.NewListCustomDomainsRequest() } c.InitBaseRequest(&request.BaseRequest, "scf", tcscf.APIVersion, "ListCustomDomains") if c.GetCredential() == nil { return nil, errors.New("ListCustomDomains require credential") } request.SetContext(ctx) response = tcscf.NewListCustomDomainsResponse() err = c.Send(request, response) return } func (c *ScfClient) UpdateCustomDomain(request *tcscf.UpdateCustomDomainRequest) (response *tcscf.UpdateCustomDomainResponse, err error) { return c.UpdateCustomDomainWithContext(context.Background(), request) } func (c *ScfClient) UpdateCustomDomainWithContext(ctx context.Context, request *tcscf.UpdateCustomDomainRequest) (response *tcscf.UpdateCustomDomainResponse, err error) { if request == nil { request = tcscf.NewUpdateCustomDomainRequest() } c.InitBaseRequest(&request.BaseRequest, "scf", tcscf.APIVersion, "UpdateCustomDomain") if c.GetCredential() == nil { return nil, errors.New("UpdateCustomDomain require credential") } request.SetContext(ctx) response = tcscf.NewUpdateCustomDomainResponse() err = c.Send(request, response) return } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-scf/tencentcloud_scf.go ================================================ package tencentcloudscf import ( "context" "errors" "fmt" "log/slog" "strings" "github.com/samber/lo" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" tcscf "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/scf/v20180416" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-scf/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) type DeployerConfig struct { // 腾讯云 SecretId。 SecretId string `json:"secretId"` // 腾讯云 SecretKey。 SecretKey string `json:"secretKey"` // 腾讯云接口端点。 Endpoint string `json:"endpoint,omitempty"` // 腾讯云地域。 Region string `json:"region"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 自定义域名(不支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.ScfClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ SecretId: config.SecretId, SecretKey: config.SecretKey, Endpoint: lo. If(strings.HasSuffix(config.Endpoint, "intl.tencentcloudapi.com"), "ssl.intl.tencentcloudapi.com"). // 国际站使用独立的接口端点 Else(""), }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no scf domains to deploy") } else { d.logger.Info("found scf domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 获取云函数自定义域名列表 // REF: https://cloud.tencent.com/document/api/583/111923 listCustomDomainsOffset := 0 listCustomDomainsLimit := 20 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } describeLiveDomainsReq := tcscf.NewListCustomDomainsRequest() describeLiveDomainsReq.Offset = common.Uint64Ptr(uint64(listCustomDomainsOffset)) describeLiveDomainsReq.Limit = common.Uint64Ptr(uint64(listCustomDomainsLimit)) describeLiveDomainsResp, err := d.sdkClient.ListCustomDomains(describeLiveDomainsReq) d.logger.Debug("sdk request 'scf.DescribeLiveDomains'", slog.Any("request", describeLiveDomainsReq), slog.Any("response", describeLiveDomainsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'scf.DescribeLiveDomains': %w", err) } if describeLiveDomainsResp.Response == nil { break } for _, domainItem := range describeLiveDomainsResp.Response.Domains { domains = append(domains, *domainItem.Domain) } if len(describeLiveDomainsResp.Response.Domains) < listCustomDomainsLimit { break } listCustomDomainsOffset++ } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error { // 查看云函数自定义域名详情 // REF: https://cloud.tencent.com/document/api/583/111924 getCustomDomainReq := tcscf.NewGetCustomDomainRequest() getCustomDomainReq.Domain = common.StringPtr(domain) getCustomDomainResp, err := d.sdkClient.GetCustomDomain(getCustomDomainReq) d.logger.Debug("sdk request 'scf.GetCustomDomain'", slog.Any("request", getCustomDomainReq), slog.Any("response", getCustomDomainResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'scf.GetCustomDomain': %w", err) } else { if getCustomDomainResp.Response.CertConfig != nil && getCustomDomainResp.Response.CertConfig.CertificateId != nil && *getCustomDomainResp.Response.CertConfig.CertificateId == cloudCertId { return nil } } // 更新云函数自定义域名 // REF: https://cloud.tencent.com/document/api/583/111922 updateCustomDomainReq := tcscf.NewUpdateCustomDomainRequest() updateCustomDomainReq.Domain = common.StringPtr(domain) updateCustomDomainReq.CertConfig = &tcscf.CertConf{ CertificateId: common.StringPtr(cloudCertId), } updateCustomDomainReq.Protocol = getCustomDomainResp.Response.Protocol if updateCustomDomainReq.Protocol == nil || *updateCustomDomainReq.Protocol == "HTTP" { updateCustomDomainReq.Protocol = common.StringPtr("HTTP&HTTPS") } updateCustomDomainResp, err := d.sdkClient.UpdateCustomDomain(updateCustomDomainReq) d.logger.Debug("sdk request 'scf.UpdateCustomDomain'", slog.Any("request", updateCustomDomainReq), slog.Any("response", updateCustomDomainResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'scf.UpdateCustomDomain': %w", err) } return nil } func createSDKClient(secretId, secretKey, endpoint, region string) (*internal.ScfClient, error) { credential := common.NewCredential(secretId, secretKey) cpf := profile.NewClientProfile() if endpoint != "" { cpf.HttpProfile.Endpoint = endpoint } client, err := internal.NewScfClient(credential, region, cpf) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-scf/tencentcloud_scf_test.go ================================================ package tencentcloudscf_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-scf" ) var ( fInputCertPath string fInputKeyPath string fSecretId string fSecretKey string fRegion string fDomain string ) func init() { argsPrefix := "TENCENTCLOUDSCF_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "") flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./tencentcloud_scf_test.go -args \ --TENCENTCLOUDSCF_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --TENCENTCLOUDSCF_INPUTKEYPATH="/path/to/your-input-key.pem" \ --TENCENTCLOUDSCF_SECRETID="your-secret-id" \ --TENCENTCLOUDSCF_SECRETKEY="your-secret-key" \ --TENCENTCLOUDSCF_REGION="ap-guangzhou" \ --TENCENTCLOUDSCF_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SECRETID: %v", fSecretId), fmt.Sprintf("SECRETKEY: %v", fSecretKey), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ SecretId: fSecretId, SecretKey: fSecretKey, Region: fRegion, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-ssl/tencentcloud_ssl.go ================================================ package tencentcloudssl import ( "context" "errors" "fmt" "log/slog" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl" "github.com/certimate-go/certimate/pkg/core/deployer" ) type DeployerConfig struct { // 腾讯云 SecretId。 SecretId string `json:"secretId"` // 腾讯云 SecretKey。 SecretKey string `json:"secretKey"` // 腾讯云接口端点。 Endpoint string `json:"endpoint,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ SecretId: config.SecretId, SecretKey: config.SecretKey, Endpoint: config.Endpoint, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } return &deployer.DeployResult{}, nil } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-ssl-deploy/internal/client.go ================================================ package internal import ( "context" "errors" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" tcssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" ) // This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/ssl/v20191205/client.go // to lightweight the vendor packages in the built binary. type SslClient struct { common.Client } func NewSslClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *SslClient, err error) { client = &SslClient{} client.Init(region). WithCredential(credential). WithProfile(clientProfile) return } func (c *SslClient) DeployCertificateInstance(request *tcssl.DeployCertificateInstanceRequest) (response *tcssl.DeployCertificateInstanceResponse, err error) { return c.DeployCertificateInstanceWithContext(context.Background(), request) } func (c *SslClient) DeployCertificateInstanceWithContext(ctx context.Context, request *tcssl.DeployCertificateInstanceRequest) (response *tcssl.DeployCertificateInstanceResponse, err error) { if request == nil { request = tcssl.NewDeployCertificateInstanceRequest() } c.InitBaseRequest(&request.BaseRequest, "ssl", tcssl.APIVersion, "DeployCertificateInstance") if c.GetCredential() == nil { return nil, errors.New("DeployCertificateInstance require credential") } request.SetContext(ctx) response = tcssl.NewDeployCertificateInstanceResponse() err = c.Send(request, response) return } func (c *SslClient) DescribeHostDeployRecordDetail(request *tcssl.DescribeHostDeployRecordDetailRequest) (response *tcssl.DescribeHostDeployRecordDetailResponse, err error) { return c.DescribeHostDeployRecordDetailWithContext(context.Background(), request) } func (c *SslClient) DescribeHostDeployRecordDetailWithContext(ctx context.Context, request *tcssl.DescribeHostDeployRecordDetailRequest) (response *tcssl.DescribeHostDeployRecordDetailResponse, err error) { if request == nil { request = tcssl.NewDescribeHostDeployRecordDetailRequest() } c.InitBaseRequest(&request.BaseRequest, "ssl", tcssl.APIVersion, "DescribeHostDeployRecordDetail") if c.GetCredential() == nil { return nil, errors.New("DescribeHostDeployRecordDetail require credential") } request.SetContext(ctx) response = tcssl.NewDescribeHostDeployRecordDetailResponse() err = c.Send(request, response) return } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-ssl-deploy/tencentcloud_ssl_deploy.go ================================================ package tencentcloudssldeploy import ( "context" "errors" "fmt" "log/slog" "time" "github.com/samber/lo" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" tcssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-ssl-deploy/internal" xwait "github.com/certimate-go/certimate/pkg/utils/wait" ) type DeployerConfig struct { // 腾讯云 SecretId。 SecretId string `json:"secretId"` // 腾讯云 SecretKey。 SecretKey string `json:"secretKey"` // 腾讯云接口端点。 Endpoint string `json:"endpoint,omitempty"` // 腾讯云地域。 Region string `json:"region"` // 云产品类型。 ResourceProduct string `json:"resourceProduct"` // 云产品资源 ID 数组。 ResourceIds []string `json:"resourceIds,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.SslClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ SecretId: config.SecretId, SecretKey: config.SecretKey, Endpoint: config.Endpoint, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { if d.config.ResourceProduct == "" { return nil, errors.New("config `resourceProduct` is required") } if len(d.config.ResourceIds) == 0 { return nil, errors.New("config `resourceIds` is required") } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 证书部署到云资源实例列表 // REF: https://cloud.tencent.com/document/api/400/91667 deployCertificateInstanceReq := tcssl.NewDeployCertificateInstanceRequest() deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId) deployCertificateInstanceReq.ResourceType = common.StringPtr(d.config.ResourceProduct) deployCertificateInstanceReq.InstanceIdList = common.StringPtrs(d.config.ResourceIds) deployCertificateInstanceReq.Status = common.Int64Ptr(1) deployCertificateInstanceResp, err := d.sdkClient.DeployCertificateInstance(deployCertificateInstanceReq) d.logger.Debug("sdk request 'ssl.DeployCertificateInstance'", slog.Any("request", deployCertificateInstanceReq), slog.Any("response", deployCertificateInstanceResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'ssl.DeployCertificateInstance': %w", err) } else if deployCertificateInstanceResp.Response == nil || deployCertificateInstanceResp.Response.DeployRecordId == nil { return nil, errors.New("failed to create deploy record") } // 获取部署任务详情,等待任务状态变更 // REF: https://cloud.tencent.com/document/api/400/91658 if _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) { describeHostDeployRecordDetailReq := tcssl.NewDescribeHostDeployRecordDetailRequest() describeHostDeployRecordDetailReq.DeployRecordId = common.StringPtr(fmt.Sprintf("%d", *deployCertificateInstanceResp.Response.DeployRecordId)) describeHostDeployRecordDetailReq.Limit = common.Uint64Ptr(200) describeHostDeployRecordDetailResp, err := d.sdkClient.DescribeHostDeployRecordDetail(describeHostDeployRecordDetailReq) d.logger.Debug("sdk request 'ssl.DescribeHostDeployRecordDetail'", slog.Any("request", describeHostDeployRecordDetailReq), slog.Any("response", describeHostDeployRecordDetailResp)) if err != nil { return false, fmt.Errorf("failed to execute sdk request 'ssl.DescribeHostDeployRecordDetail': %w", err) } var pendingCount, runningCount, succeededCount, failedCount, totalCount int64 if describeHostDeployRecordDetailResp.Response.TotalCount == nil { return false, fmt.Errorf("unexpected tencentcloud deployment job status") } else { pendingCount = lo.FromPtr(describeHostDeployRecordDetailResp.Response.PendingTotalCount) runningCount = lo.FromPtr(describeHostDeployRecordDetailResp.Response.RunningTotalCount) succeededCount = lo.FromPtr(describeHostDeployRecordDetailResp.Response.SuccessTotalCount) failedCount = lo.FromPtr(describeHostDeployRecordDetailResp.Response.FailedTotalCount) totalCount = lo.FromPtr(describeHostDeployRecordDetailResp.Response.TotalCount) if succeededCount+failedCount == totalCount { if failedCount > 0 { return false, fmt.Errorf("tencentcloud deployment job failed (succeeded: %d, failed: %d, total: %d)", succeededCount, failedCount, totalCount) } return true, nil } } d.logger.Info(fmt.Sprintf("waiting for tencentcloud deployment job completion (pending: %d, running: %d, succeeded: %d, failed: %d, total: %d) ...", pendingCount, runningCount, succeededCount, failedCount, totalCount)) return false, nil }, time.Second*5); err != nil { return nil, err } return &deployer.DeployResult{}, nil } func createSDKClient(secretId, secretKey, endpoint, region string) (*internal.SslClient, error) { credential := common.NewCredential(secretId, secretKey) cpf := profile.NewClientProfile() if endpoint != "" { cpf.HttpProfile.Endpoint = endpoint } client, err := internal.NewSslClient(credential, region, cpf) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-ssl-update/internal/client.go ================================================ package internal import ( "context" "errors" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" tcssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" ) // This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/ssl/v20191205/client.go // to lightweight the vendor packages in the built binary. type SslClient struct { common.Client } func NewSslClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *SslClient, err error) { client = &SslClient{} client.Init(region). WithCredential(credential). WithProfile(clientProfile) return } func (c *SslClient) DescribeHostUpdateRecordDetail(request *tcssl.DescribeHostUpdateRecordDetailRequest) (response *tcssl.DescribeHostUpdateRecordDetailResponse, err error) { return c.DescribeHostUpdateRecordDetailWithContext(context.Background(), request) } func (c *SslClient) DescribeHostUpdateRecordDetailWithContext(ctx context.Context, request *tcssl.DescribeHostUpdateRecordDetailRequest) (response *tcssl.DescribeHostUpdateRecordDetailResponse, err error) { if request == nil { request = tcssl.NewDescribeHostUpdateRecordDetailRequest() } c.InitBaseRequest(&request.BaseRequest, "ssl", tcssl.APIVersion, "DescribeHostUpdateRecordDetail") if c.GetCredential() == nil { return nil, errors.New("DescribeHostUpdateRecordDetail require credential") } request.SetContext(ctx) response = tcssl.NewDescribeHostUpdateRecordDetailResponse() err = c.Send(request, response) return } func (c *SslClient) DescribeHostUploadUpdateRecordDetail(request *tcssl.DescribeHostUploadUpdateRecordDetailRequest) (response *tcssl.DescribeHostUploadUpdateRecordDetailResponse, err error) { return c.DescribeHostUploadUpdateRecordDetailWithContext(context.Background(), request) } func (c *SslClient) DescribeHostUploadUpdateRecordDetailWithContext(ctx context.Context, request *tcssl.DescribeHostUploadUpdateRecordDetailRequest) (response *tcssl.DescribeHostUploadUpdateRecordDetailResponse, err error) { if request == nil { request = tcssl.NewDescribeHostUploadUpdateRecordDetailRequest() } c.InitBaseRequest(&request.BaseRequest, "ssl", tcssl.APIVersion, "DescribeHostUploadUpdateRecordDetail") if c.GetCredential() == nil { return nil, errors.New("DescribeHostUploadUpdateRecordDetail require credential") } request.SetContext(ctx) response = tcssl.NewDescribeHostUploadUpdateRecordDetailResponse() err = c.Send(request, response) return } func (c *SslClient) UpdateCertificateInstance(request *tcssl.UpdateCertificateInstanceRequest) (response *tcssl.UpdateCertificateInstanceResponse, err error) { return c.UpdateCertificateInstanceWithContext(context.Background(), request) } func (c *SslClient) UpdateCertificateInstanceWithContext(ctx context.Context, request *tcssl.UpdateCertificateInstanceRequest) (response *tcssl.UpdateCertificateInstanceResponse, err error) { if request == nil { request = tcssl.NewUpdateCertificateInstanceRequest() } c.InitBaseRequest(&request.BaseRequest, "ssl", tcssl.APIVersion, "UpdateCertificateInstance") if c.GetCredential() == nil { return nil, errors.New("UpdateCertificateInstance require credential") } request.SetContext(ctx) response = tcssl.NewUpdateCertificateInstanceResponse() err = c.Send(request, response) return } func (c *SslClient) UploadUpdateCertificateInstance(request *tcssl.UploadUpdateCertificateInstanceRequest) (response *tcssl.UploadUpdateCertificateInstanceResponse, err error) { return c.UploadUpdateCertificateInstanceWithContext(context.Background(), request) } func (c *SslClient) UploadUpdateCertificateInstanceWithContext(ctx context.Context, request *tcssl.UploadUpdateCertificateInstanceRequest) (response *tcssl.UploadUpdateCertificateInstanceResponse, err error) { if request == nil { request = tcssl.NewUploadUpdateCertificateInstanceRequest() } c.InitBaseRequest(&request.BaseRequest, "ssl", tcssl.APIVersion, "UploadUpdateCertificateInstance") if c.GetCredential() == nil { return nil, errors.New("UploadUpdateCertificateInstance require credential") } request.SetContext(ctx) response = tcssl.NewUploadUpdateCertificateInstanceResponse() err = c.Send(request, response) return } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-ssl-update/tencentcloud_ssl_update.go ================================================ package tencentcloudsslupdate import ( "context" "errors" "fmt" "log/slog" "slices" "time" "github.com/samber/lo" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" tcssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-ssl-update/internal" xwait "github.com/certimate-go/certimate/pkg/utils/wait" ) type DeployerConfig struct { // 腾讯云 SecretId。 SecretId string `json:"secretId"` // 腾讯云 SecretKey。 SecretKey string `json:"secretKey"` // 腾讯云接口端点。 Endpoint string `json:"endpoint,omitempty"` // 原证书 ID。 CertificateId string `json:"certificateId"` // 是否替换原有证书(即保持原证书 ID 不变)。 IsReplaced bool `json:"isReplaced,omitempty"` // 云产品类型数组。 ResourceProducts []string `json:"resourceProducts"` // 云产品地域数组。 ResourceRegions []string `json:"resourceRegions"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.SslClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ SecretId: config.SecretId, SecretKey: config.SecretKey, Endpoint: config.Endpoint, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { if d.config.CertificateId == "" { return nil, errors.New("config `certificateId` is required") } if len(d.config.ResourceProducts) == 0 { return nil, errors.New("config `resourceProducts` is required") } if d.config.IsReplaced { if err := d.executeUploadUpdateCertificateInstance(ctx, certPEM, privkeyPEM); err != nil { return nil, err } } else { if err := d.executeUpdateCertificateInstance(ctx, certPEM, privkeyPEM); err != nil { return nil, err } } return &deployer.DeployResult{}, nil } func (d *Deployer) executeUpdateCertificateInstance(ctx context.Context, certPEM, privkeyPEM string) error { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 一键更新新旧证书资源 // REF: https://cloud.tencent.com/document/product/400/91649 var deployRecordId string if _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) { updateCertificateInstanceReq := tcssl.NewUpdateCertificateInstanceRequest() updateCertificateInstanceReq.OldCertificateId = common.StringPtr(d.config.CertificateId) updateCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId) updateCertificateInstanceReq.ResourceTypes = common.StringPtrs(d.config.ResourceProducts) updateCertificateInstanceReq.ResourceTypesRegions = wrapResourceProductRegions(d.config.ResourceProducts, d.config.ResourceRegions) updateCertificateInstanceResp, err := d.sdkClient.UpdateCertificateInstance(updateCertificateInstanceReq) d.logger.Debug("sdk request 'ssl.UpdateCertificateInstance'", slog.Any("request", updateCertificateInstanceReq), slog.Any("response", updateCertificateInstanceResp)) if err != nil { return false, fmt.Errorf("failed to execute sdk request 'ssl.UpdateCertificateInstance': %w", err) } if updateCertificateInstanceResp.Response.DeployStatus == nil || updateCertificateInstanceResp.Response.DeployRecordId == nil { return false, fmt.Errorf("unexpected deployment job status") } else if *updateCertificateInstanceResp.Response.DeployRecordId > 0 { deployRecordId = fmt.Sprintf("%d", *updateCertificateInstanceResp.Response.DeployRecordId) return true, nil } return false, nil }, time.Second*5); err != nil { return err } // 查询证书云资源更新记录详情,等待任务状态变更 // REF: https://cloud.tencent.com/document/api/400/91652 if _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) { describeHostUpdateRecordDetailReq := tcssl.NewDescribeHostUpdateRecordDetailRequest() describeHostUpdateRecordDetailReq.DeployRecordId = common.StringPtr(deployRecordId) describeHostUpdateRecordDetailReq.Limit = common.StringPtr("200") describeHostUpdateRecordDetailResp, err := d.sdkClient.DescribeHostUpdateRecordDetail(describeHostUpdateRecordDetailReq) d.logger.Debug("sdk request 'ssl.DescribeHostUpdateRecordDetail'", slog.Any("request", describeHostUpdateRecordDetailReq), slog.Any("response", describeHostUpdateRecordDetailResp)) if err != nil { return false, fmt.Errorf("failed to execute sdk request 'ssl.DescribeHostUpdateRecordDetail': %w", err) } var pendingCount, runningCount, succeededCount, failedCount, totalCount int64 if describeHostUpdateRecordDetailResp.Response.TotalCount == nil { return false, fmt.Errorf("unexpected tencentcloud deployment job status") } else { pendingCount = lo.FromPtr(describeHostUpdateRecordDetailResp.Response.PendingTotalCount) runningCount = lo.FromPtr(describeHostUpdateRecordDetailResp.Response.RunningTotalCount) succeededCount = lo.FromPtr(describeHostUpdateRecordDetailResp.Response.SuccessTotalCount) failedCount = lo.FromPtr(describeHostUpdateRecordDetailResp.Response.FailedTotalCount) totalCount = lo.FromPtr(describeHostUpdateRecordDetailResp.Response.TotalCount) if succeededCount+failedCount == totalCount { if failedCount > 0 { return false, fmt.Errorf("tencentcloud deployment job failed (succeeded: %d, failed: %d, total: %d)", succeededCount, failedCount, totalCount) } return true, nil } } d.logger.Info(fmt.Sprintf("waiting for tencentcloud deployment job completion (pending: %d, running: %d, succeeded: %d, failed: %d, total: %d) ...", pendingCount, runningCount, succeededCount, failedCount, totalCount)) return false, nil }, time.Second*5); err != nil { return err } return nil } func (d *Deployer) executeUploadUpdateCertificateInstance(ctx context.Context, certPEM, privkeyPEM string) error { // 更新证书内容并更新关联的云资源 // REF: https://cloud.tencent.com/document/product/400/119791 var deployRecordId int64 if _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) { uploadUpdateCertificateInstanceReq := tcssl.NewUploadUpdateCertificateInstanceRequest() uploadUpdateCertificateInstanceReq.OldCertificateId = common.StringPtr(d.config.CertificateId) uploadUpdateCertificateInstanceReq.CertificatePublicKey = common.StringPtr(certPEM) uploadUpdateCertificateInstanceReq.CertificatePrivateKey = common.StringPtr(privkeyPEM) uploadUpdateCertificateInstanceReq.ResourceTypes = common.StringPtrs(d.config.ResourceProducts) uploadUpdateCertificateInstanceReq.ResourceTypesRegions = wrapResourceProductRegions(d.config.ResourceProducts, d.config.ResourceRegions) uploadUpdateCertificateInstanceResp, err := d.sdkClient.UploadUpdateCertificateInstance(uploadUpdateCertificateInstanceReq) d.logger.Debug("sdk request 'ssl.UploadUpdateCertificateInstance'", slog.Any("request", uploadUpdateCertificateInstanceReq), slog.Any("response", uploadUpdateCertificateInstanceResp)) if err != nil { return false, fmt.Errorf("failed to execute sdk request 'ssl.UploadUpdateCertificateInstance': %w", err) } if uploadUpdateCertificateInstanceResp.Response.DeployStatus == nil { return false, fmt.Errorf("unexpected deployment job status") } else if *uploadUpdateCertificateInstanceResp.Response.DeployStatus == 1 { deployRecordId = int64(*uploadUpdateCertificateInstanceResp.Response.DeployRecordId) return true, nil } return false, nil }, time.Second*5); err != nil { return err } // 查询证书云资源更新记录详情,等待任务状态变更 // REF: https://cloud.tencent.com/document/product/400/120056 if _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) { describeHostUploadUpdateRecordDetailReq := tcssl.NewDescribeHostUploadUpdateRecordDetailRequest() describeHostUploadUpdateRecordDetailReq.DeployRecordId = common.Int64Ptr(deployRecordId) describeHostUploadUpdateRecordDetailReq.Limit = common.Int64Ptr(200) describeHostUploadUpdateRecordDetailResp, err := d.sdkClient.DescribeHostUploadUpdateRecordDetail(describeHostUploadUpdateRecordDetailReq) d.logger.Debug("sdk request 'ssl.DescribeHostUploadUpdateRecordDetail'", slog.Any("request", describeHostUploadUpdateRecordDetailReq), slog.Any("response", describeHostUploadUpdateRecordDetailResp)) if err != nil { return false, fmt.Errorf("failed to execute sdk request 'ssl.DescribeHostUploadUpdateRecordDetail': %w", err) } var runningCount, succeededCount, failedCount, totalCount int64 if describeHostUploadUpdateRecordDetailResp.Response.DeployRecordDetail == nil { return false, fmt.Errorf("unexpected tencentcloud deployment job status") } else { for _, record := range describeHostUploadUpdateRecordDetailResp.Response.DeployRecordDetail { runningCount += lo.FromPtr(record.RunningTotalCount) succeededCount += lo.FromPtr(record.SuccessTotalCount) failedCount += lo.FromPtr(record.FailedTotalCount) totalCount += lo.FromPtr(record.TotalCount) } if succeededCount+failedCount == totalCount { if failedCount > 0 { return false, fmt.Errorf("tencentcloud deployment job failed (succeeded: %d, failed: %d, total: %d)", succeededCount, failedCount, totalCount) } return true, nil } } d.logger.Info(fmt.Sprintf("waiting for tencentcloud deployment job completion (running: %d, succeeded: %d, failed: %d, total: %d) ...", runningCount, succeededCount, failedCount, totalCount)) return false, nil }, time.Second*5); err != nil { return err } return nil } func createSDKClient(secretId, secretKey, endpoint string) (*internal.SslClient, error) { credential := common.NewCredential(secretId, secretKey) cpf := profile.NewClientProfile() if endpoint != "" { cpf.HttpProfile.Endpoint = endpoint } client, err := internal.NewSslClient(credential, "", cpf) if err != nil { return nil, err } return client, nil } func wrapResourceProductRegions(resourceProducts, resourceRegions []string) []*tcssl.ResourceTypeRegions { if len(resourceProducts) == 0 || len(resourceRegions) == 0 { return nil } // 仅以下云产品类型支持地域 resourceProductsRequireRegion := []string{"apigateway", "clb", "cos", "tcb", "tke", "tse", "waf"} temp := make([]*tcssl.ResourceTypeRegions, 0) for _, resourceProduct := range resourceProducts { if slices.Contains(resourceProductsRequireRegion, resourceProduct) { temp = append(temp, &tcssl.ResourceTypeRegions{ ResourceType: common.StringPtr(resourceProduct), Regions: common.StringPtrs(resourceRegions), }) } } return temp } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-vod/consts.go ================================================ package tencentcloudvod const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/tencentcloud-vod/internal/client.go ================================================ package internal import ( "context" "errors" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" tcvod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vod/v20180717" ) // This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/vod/v20180717/client.go // to lightweight the vendor packages in the built binary. type VodClient struct { common.Client } func NewVodClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *VodClient, err error) { client = &VodClient{} client.Init(region). WithCredential(credential). WithProfile(clientProfile) return } func (c *VodClient) DescribeVodDomains(request *tcvod.DescribeVodDomainsRequest) (response *tcvod.DescribeVodDomainsResponse, err error) { return c.DescribeVodDomainsWithContext(context.Background(), request) } func (c *VodClient) DescribeVodDomainsWithContext(ctx context.Context, request *tcvod.DescribeVodDomainsRequest) (response *tcvod.DescribeVodDomainsResponse, err error) { if request == nil { request = tcvod.NewDescribeVodDomainsRequest() } c.InitBaseRequest(&request.BaseRequest, "vod", tcvod.APIVersion, "DescribeVodDomains") if c.GetCredential() == nil { return nil, errors.New("DescribeVodDomains require credential") } request.SetContext(ctx) response = tcvod.NewDescribeVodDomainsResponse() err = c.Send(request, response) return } func (c *VodClient) SetVodDomainCertificate(request *tcvod.SetVodDomainCertificateRequest) (response *tcvod.SetVodDomainCertificateResponse, err error) { return c.SetVodDomainCertificateWithContext(context.Background(), request) } func (c *VodClient) SetVodDomainCertificateWithContext(ctx context.Context, request *tcvod.SetVodDomainCertificateRequest) (response *tcvod.SetVodDomainCertificateResponse, err error) { if request == nil { request = tcvod.NewSetVodDomainCertificateRequest() } c.InitBaseRequest(&request.BaseRequest, "vod", tcvod.APIVersion, "SetVodDomainCertificate") if c.GetCredential() == nil { return nil, errors.New("SetVodDomainCertificate require credential") } request.SetContext(ctx) response = tcvod.NewSetVodDomainCertificateResponse() err = c.Send(request, response) return } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-vod/tencentcloud_vod.go ================================================ package tencentcloudvod import ( "context" "errors" "fmt" "log/slog" "strings" "github.com/samber/lo" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" tcvod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vod/v20180717" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-vod/internal" ) type DeployerConfig struct { // 腾讯云 SecretId。 SecretId string `json:"secretId"` // 腾讯云 SecretKey。 SecretKey string `json:"secretKey"` // 腾讯云接口端点。 Endpoint string `json:"endpoint,omitempty"` // 点播应用 ID。 SubAppId int64 `json:"subAppId"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 点播加速域名(不支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.VodClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ SecretId: config.SecretId, SecretKey: config.SecretKey, Endpoint: lo. If(strings.HasSuffix(config.Endpoint, "intl.tencentcloudapi.com"), "ssl.intl.tencentcloudapi.com"). // 国际站使用独立的接口端点 Else(""), }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的 ECDN 实例 domains := make([]string, 0) switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_CERTSAN: { domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = domainCandidates } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no vod domains to deploy") } else { d.logger.Info("found vod domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 查询点播域名列表 // REF: https://cloud.tencent.com/document/api/266/54176 describeVodDomainsOffset := 0 describeVodDomainsLimit := 20 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } describeVodDomainsReq := tcvod.NewDescribeVodDomainsRequest() describeVodDomainsReq.Offset = common.Uint64Ptr(uint64(describeVodDomainsOffset)) describeVodDomainsReq.Limit = common.Uint64Ptr(uint64(describeVodDomainsLimit)) if d.config.SubAppId != 0 { describeVodDomainsReq.SubAppId = common.Uint64Ptr(uint64(d.config.SubAppId)) } describeVodDomainsResp, err := d.sdkClient.DescribeVodDomains(describeVodDomainsReq) d.logger.Debug("sdk request 'vod.DescribeVodDomains'", slog.Any("request", describeVodDomainsReq), slog.Any("response", describeVodDomainsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'vod.DescribeVodDomains': %w", err) } if describeVodDomainsResp.Response == nil { break } ignoredStatuses := []string{"Locked"} for _, domainItem := range describeVodDomainsResp.Response.DomainSet { if lo.Contains(ignoredStatuses, *domainItem.DeployStatus) { continue } domains = append(domains, *domainItem.Domain) } if len(describeVodDomainsResp.Response.DomainSet) < describeVodDomainsLimit { break } describeVodDomainsOffset += describeVodDomainsLimit } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error { // 设置点播域名 HTTPS 证书 // REF: https://cloud.tencent.com/document/api/266/102015 setVodDomainCertificateReq := tcvod.NewSetVodDomainCertificateRequest() setVodDomainCertificateReq.Domain = common.StringPtr(domain) setVodDomainCertificateReq.Operation = common.StringPtr("Set") setVodDomainCertificateReq.CertID = common.StringPtr(cloudCertId) if d.config.SubAppId != 0 { setVodDomainCertificateReq.SubAppId = common.Uint64Ptr(uint64(d.config.SubAppId)) } setVodDomainCertificateResp, err := d.sdkClient.SetVodDomainCertificate(setVodDomainCertificateReq) d.logger.Debug("sdk request 'vod.SetVodDomainCertificate'", slog.Any("request", setVodDomainCertificateReq), slog.Any("response", setVodDomainCertificateResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'vod.SetVodDomainCertificate': %w", err) } return nil } func createSDKClient(secretId, secretKey, endpoint string) (*internal.VodClient, error) { credential := common.NewCredential(secretId, secretKey) cpf := profile.NewClientProfile() if endpoint != "" { cpf.HttpProfile.Endpoint = endpoint } client, err := internal.NewVodClient(credential, "", cpf) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-vod/tencentcloud_vod_test.go ================================================ package tencentcloudvod_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-vod" ) var ( fInputCertPath string fInputKeyPath string fSecretId string fSecretKey string fDomain string fSubAppId int64 fInstanceId string ) func init() { argsPrefix := "TENCENTCLOUDVOD_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "") flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") flag.Int64Var(&fSubAppId, argsPrefix+"SUBAPPID", 0, "") flag.StringVar(&fInstanceId, argsPrefix+"INSTANCEID", "", "") } /* Shell command to run this test: go test -v ./tencentcloud_vod_test.go -args \ --TENCENTCLOUDVOD_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --TENCENTCLOUDVOD_INPUTKEYPATH="/path/to/your-input-key.pem" \ --TENCENTCLOUDVOD_SECRETID="your-secret-id" \ --TENCENTCLOUDVOD_SECRETKEY="your-secret-key" \ --TENCENTCLOUDVOD_SUBAPPID="your-app-id" \ --TENCENTCLOUDVOD_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SECRETID: %v", fSecretId), fmt.Sprintf("SECRETKEY: %v", fSecretKey), fmt.Sprintf("DOMAIN: %v", fDomain), fmt.Sprintf("INSTANCEID: %v", fInstanceId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ SecretId: fSecretId, SecretKey: fSecretKey, SubAppId: fSubAppId, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-waf/internal/client.go ================================================ package internal import ( "context" "errors" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" tcwaf "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/waf/v20180125" ) // This is a partial copy of https://github.com/TencentCloud/tencentcloud-sdk-go/blob/master/tencentcloud/waf/v20180125/client.go // to lightweight the vendor packages in the built binary. type WafClient struct { common.Client } func NewWafClient(credential common.CredentialIface, region string, clientProfile *profile.ClientProfile) (client *WafClient, err error) { client = &WafClient{} client.Init(region). WithCredential(credential). WithProfile(clientProfile) return } func (c *WafClient) DescribeDomainDetailsSaas(request *tcwaf.DescribeDomainDetailsSaasRequest) (response *tcwaf.DescribeDomainDetailsSaasResponse, err error) { return c.DescribeDomainDetailsSaasWithContext(context.Background(), request) } func (c *WafClient) DescribeDomainDetailsSaasWithContext(ctx context.Context, request *tcwaf.DescribeDomainDetailsSaasRequest) (response *tcwaf.DescribeDomainDetailsSaasResponse, err error) { if request == nil { request = tcwaf.NewDescribeDomainDetailsSaasRequest() } c.InitBaseRequest(&request.BaseRequest, "waf", tcwaf.APIVersion, "DescribeDomainDetailsSaas") if c.GetCredential() == nil { return nil, errors.New("DescribeDomainDetailsSaas require credential") } request.SetContext(ctx) response = tcwaf.NewDescribeDomainDetailsSaasResponse() err = c.Send(request, response) return } func (c *WafClient) ModifySpartaProtection(request *tcwaf.ModifySpartaProtectionRequest) (response *tcwaf.ModifySpartaProtectionResponse, err error) { return c.ModifySpartaProtectionWithContext(context.Background(), request) } func (c *WafClient) ModifySpartaProtectionWithContext(ctx context.Context, request *tcwaf.ModifySpartaProtectionRequest) (response *tcwaf.ModifySpartaProtectionResponse, err error) { if request == nil { request = tcwaf.NewModifySpartaProtectionRequest() } c.InitBaseRequest(&request.BaseRequest, "waf", tcwaf.APIVersion, "ModifySpartaProtection") if c.GetCredential() == nil { return nil, errors.New("ModifySpartaProtection require credential") } request.SetContext(ctx) response = tcwaf.NewModifySpartaProtectionResponse() err = c.Send(request, response) return } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-waf/tencentcloud_waf.go ================================================ package tencentcloudwaf import ( "context" "errors" "fmt" "log/slog" "strings" "github.com/samber/lo" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" tcwaf "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/waf/v20180125" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/tencentcloud-ssl" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-waf/internal" ) type DeployerConfig struct { // 腾讯云 SecretId。 SecretId string `json:"secretId"` // 腾讯云 SecretKey。 SecretKey string `json:"secretKey"` // 腾讯云接口端点。 Endpoint string `json:"endpoint,omitempty"` // 腾讯云地域。 Region string `json:"region"` // 防护域名(不支持泛域名)。 Domain string `json:"domain"` // 防护域名 ID。 DomainId string `json:"domainId"` // 防护域名所属实例 ID。 InstanceId string `json:"instanceId"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.WafClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.SecretId, config.SecretKey, config.Endpoint, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ SecretId: config.SecretId, SecretKey: config.SecretKey, Endpoint: lo. If(strings.HasSuffix(config.Endpoint, "intl.tencentcloudapi.com"), "ssl.intl.tencentcloudapi.com"). // 国际站使用独立的接口端点 Else(""), }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } if d.config.DomainId == "" { return nil, errors.New("config `domainId` is required") } if d.config.InstanceId == "" { return nil, errors.New("config `instanceId` is required") } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 查询单个 SaaS 型 WAF 域名详情 // REF: https://cloud.tencent.com/document/api/627/82938 describeDomainDetailsSaasReq := tcwaf.NewDescribeDomainDetailsSaasRequest() describeDomainDetailsSaasReq.Domain = common.StringPtr(d.config.Domain) describeDomainDetailsSaasReq.DomainId = common.StringPtr(d.config.DomainId) describeDomainDetailsSaasReq.InstanceId = common.StringPtr(d.config.InstanceId) describeDomainDetailsSaasResp, err := d.sdkClient.DescribeDomainDetailsSaas(describeDomainDetailsSaasReq) d.logger.Debug("sdk request 'waf.DescribeDomainDetailsSaas'", slog.Any("request", describeDomainDetailsSaasReq), slog.Any("response", describeDomainDetailsSaasResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'waf.DescribeDomainDetailsSaas': %w", err) } // 编辑 SaaS 型 WAF 域名 // REF: https://cloud.tencent.com/document/api/627/94309 modifySpartaProtectionReq := tcwaf.NewModifySpartaProtectionRequest() modifySpartaProtectionReq.Domain = common.StringPtr(d.config.Domain) modifySpartaProtectionReq.DomainId = common.StringPtr(d.config.DomainId) modifySpartaProtectionReq.InstanceID = common.StringPtr(d.config.InstanceId) modifySpartaProtectionReq.CertType = common.Int64Ptr(2) modifySpartaProtectionReq.SSLId = common.StringPtr(upres.CertId) modifySpartaProtectionResp, err := d.sdkClient.ModifySpartaProtection(modifySpartaProtectionReq) d.logger.Debug("sdk request 'waf.ModifySpartaProtection'", slog.Any("request", modifySpartaProtectionReq), slog.Any("response", modifySpartaProtectionResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'waf.ModifySpartaProtection': %w", err) } return &deployer.DeployResult{}, nil } func createSDKClient(secretId, secretKey, endpoint, region string) (*internal.WafClient, error) { credential := common.NewCredential(secretId, secretKey) cpf := profile.NewClientProfile() if endpoint != "" { cpf.HttpProfile.Endpoint = endpoint } client, err := internal.NewWafClient(credential, region, cpf) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/tencentcloud-waf/tencentcloud_waf_test.go ================================================ package tencentcloudwaf_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/tencentcloud-waf" ) var ( fInputCertPath string fInputKeyPath string fSecretId string fSecretKey string fRegion string fDomain string fDomainId string fInstanceId string ) func init() { argsPrefix := "TENCENTCLOUDWAF_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "") flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") flag.StringVar(&fDomainId, argsPrefix+"DOMAINID", "", "") flag.StringVar(&fInstanceId, argsPrefix+"INSTANCEID", "", "") } /* Shell command to run this test: go test -v ./tencentcloud_waf_test.go -args \ --TENCENTCLOUDWAF_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --TENCENTCLOUDWAF_INPUTKEYPATH="/path/to/your-input-key.pem" \ --TENCENTCLOUDWAF_SECRETID="your-secret-id" \ --TENCENTCLOUDWAF_SECRETKEY="your-secret-key" \ --TENCENTCLOUDWAF_REGION="ap-guangzhou" \ --TENCENTCLOUDWAF_DOMAIN="example.com" \ --TENCENTCLOUDWAF_DOMAINID="your-domain-id" \ --TENCENTCLOUDWAF_INSTANCEID="your-instance-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("SECRETID: %v", fSecretId), fmt.Sprintf("SECRETKEY: %v", fSecretKey), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("DOMAIN: %v", fDomain), fmt.Sprintf("INSTANCEID: %v", fInstanceId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ SecretId: fSecretId, SecretKey: fSecretKey, Region: fRegion, Domain: fDomain, DomainId: fDomainId, InstanceId: fInstanceId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/ucloud-ualb/consts.go ================================================ package ucloudualb const ( // 资源类型:部署到指定负载均衡器。 RESOURCE_TYPE_LOADBALANCER = "loadbalancer" // 资源类型:部署到指定监听器。 RESOURCE_TYPE_LISTENER = "listener" ) ================================================ FILE: pkg/core/deployer/providers/ucloud-ualb/ucloud_ualb.go ================================================ package ucloudualb import ( "context" "errors" "fmt" "log/slog" "time" "github.com/samber/lo" "github.com/ucloud/ucloud-sdk-go/services/ulb" "github.com/ucloud/ucloud-sdk-go/ucloud" "github.com/ucloud/ucloud-sdk-go/ucloud/auth" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ucloud-ulb" "github.com/certimate-go/certimate/pkg/core/deployer" ucloudsdk "github.com/certimate-go/certimate/pkg/sdk3rd/ucloud/ulb" ) type DeployerConfig struct { // 优刻得 API 私钥。 PrivateKey string `json:"privateKey"` // 优刻得 API 公钥。 PublicKey string `json:"publicKey"` // 优刻得项目 ID。 ProjectId string `json:"projectId,omitempty"` // 优刻得地域。 Region string `json:"region"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 负载均衡实例 ID。 // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时必填。 LoadbalancerId string `json:"loadbalancerId,omitempty"` // 负载均衡监听器 ID。 // 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。 ListenerId string `json:"listenerId,omitempty"` // SNI 域名(不支持泛域名)。 // 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时选填。 Domain string `json:"domain,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *ucloudsdk.ULBClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.PrivateKey, config.PublicKey, config.ProjectId, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ PrivateKey: config.PrivateKey, PublicKey: config.PublicKey, ProjectId: config.ProjectId, Region: config.Region, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_LOADBALANCER: if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil { return nil, err } case RESOURCE_TYPE_LISTENER: if err := d.deployToListener(ctx, upres.CertId); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error { if d.config.LoadbalancerId == "" { return errors.New("config `loadbalancerId` is required") } // 获取 ALB 下的 HTTPS 监听器列表 // REF: https://docs.ucloud.cn/api/ulb-api/describe_listeners listenerIds := make([]string, 0) describeListenersOffset := 0 describeListenersLimit := 100 for { select { case <-ctx.Done(): return ctx.Err() default: } describeListenerReq := d.sdkClient.NewDescribeListenersRequest() describeListenerReq.LoadBalancerId = ucloud.String(d.config.LoadbalancerId) describeListenerReq.Offset = ucloud.Int(describeListenersOffset) describeListenerReq.Limit = ucloud.Int(describeListenersLimit) describeListenerResp, err := d.sdkClient.DescribeListeners(describeListenerReq) d.logger.Debug("sdk request 'ulb.DescribeListeners'", slog.Any("request", describeListenerReq), slog.Any("response", describeListenerResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'ulb.DescribeListeners': %w", err) } for _, listenerItem := range describeListenerResp.Listeners { if listenerItem.ListenerProtocol == "HTTPS" { listenerIds = append(listenerIds, listenerItem.ListenerId) } } if len(describeListenerResp.Listeners) < describeListenersLimit { break } describeListenersOffset += describeListenersLimit } // 遍历更新 Listener 证书 if len(listenerIds) == 0 { d.logger.Info("no alb listeners to deploy") } else { d.logger.Info("found https listeners to deploy", slog.Any("listenerIds", listenerIds)) var errs []error for _, listenerId := range listenerIds { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listenerId, cloudCertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error { if d.config.LoadbalancerId == "" { return errors.New("config `loadbalancerId` is required") } if d.config.ListenerId == "" { return errors.New("config `listenerId` is required") } if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, d.config.ListenerId, cloudCertId); err != nil { return err } return nil } func (d *Deployer) updateListenerCertificate(ctx context.Context, cloudLoadbalancerId, cloudListenerId string, cloudCertId string) error { // 描述应用型负载均衡监听器 // REF: https://docs.ucloud.cn/api/ulb-api/describe_listeners describeListenersReq := d.sdkClient.NewDescribeListenersRequest() describeListenersReq.LoadBalancerId = ucloud.String(cloudLoadbalancerId) describeListenersReq.ListenerId = ucloud.String(cloudListenerId) describeListenersReq.Limit = ucloud.Int(1) describeListenerResp, err := d.sdkClient.DescribeListeners(describeListenersReq) d.logger.Debug("sdk request 'ulb.DescribeListeners'", slog.Any("request", describeListenersReq), slog.Any("response", describeListenerResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'ulb.DescribeListeners': %w", err) } else if len(describeListenerResp.Listeners) == 0 { return fmt.Errorf("could not find listener '%s'", cloudListenerId) } // 跳过已部署过的监听器 listenerInfo := describeListenerResp.Listeners[0] if d.config.Domain == "" { if lo.ContainsBy(listenerInfo.Certificates, func(item ulb.Certificate) bool { return item.SSLId == cloudCertId && item.IsDefault }) { return nil } } else { if lo.ContainsBy(listenerInfo.Certificates, func(item ulb.Certificate) bool { return item.SSLId == cloudCertId && !item.IsDefault }) { return nil } } if d.config.Domain == "" { // 未指定 SNI,只需部署到监听器 updateListenerAttributeReq := d.sdkClient.NewUpdateListenerAttributeRequest() updateListenerAttributeReq.LoadBalancerId = ucloud.String(cloudLoadbalancerId) updateListenerAttributeReq.ListenerId = ucloud.String(cloudListenerId) updateListenerAttributeReq.Certificates = []string{cloudCertId} updateListenerResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq) d.logger.Debug("sdk request 'ulb.UpdateListenerAttribute'", slog.Any("request", updateListenerAttributeReq), slog.Any("response", updateListenerResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'ulb.UpdateListenerAttribute': %w", err) } } else { // 指定 SNI,需部署到扩展域名 // 新增监听器扩展证书 // REF: https://docs.ucloud.cn/api/ulb-api/add_ssl_binding_json addSSLBindingReq := d.sdkClient.NewAddSSLBindingRequest() addSSLBindingReq.LoadBalancerId = ucloud.String(cloudLoadbalancerId) addSSLBindingReq.ListenerId = ucloud.String(cloudListenerId) addSSLBindingReq.SSLIds = []string{cloudCertId} addSSLBindingResp, err := d.sdkClient.AddSSLBinding(addSSLBindingReq) d.logger.Debug("sdk request 'ulb.AddSSLBinding'", slog.Any("request", addSSLBindingReq), slog.Any("response", addSSLBindingResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'ulb.AddSSLBinding': %w", err) } // 找出需要删除绑定的扩展证书 // REF: https://docs.ucloud.cn/api/ulb-api/describe_sslv2 sslIdsToDelete := make([]string, 0) for _, certItem := range listenerInfo.Certificates { if certItem.IsDefault { continue } describeSSLV2Req := d.sdkClient.NewDescribeSSLV2Request() describeSSLV2Req.SSLId = ucloud.String(certItem.SSLId) describeSSLV2Req.Limit = ucloud.Int(1) describeSSLV2Resp, err := d.sdkClient.DescribeSSLV2(describeSSLV2Req) d.logger.Debug("sdk request 'ulb.DescribeSSLV2'", slog.Any("request", describeSSLV2Req), slog.Any("response", describeSSLV2Resp)) if err != nil { continue } else if len(describeSSLV2Resp.DataSet) == 0 { continue } sslItem := describeSSLV2Resp.DataSet[0] if sslItem.NotAfter != 0 && int64(sslItem.NotAfter) < time.Now().Unix() { sslIdsToDelete = append(sslIdsToDelete, sslItem.SSLId) // 过期证书需要删除 continue } else if sslItem.Domains == d.config.Domain { sslIdsToDelete = append(sslIdsToDelete, sslItem.SSLId) // 同域名证书需要删除 continue } } // 删除监听器绑定的扩展证书 // REF: https://docs.ucloud.cn/api/ulb-api/delete_ssl_binding_json if len(sslIdsToDelete) > 0 { deleteSSLBindingReq := d.sdkClient.NewDeleteSSLBindingRequest() deleteSSLBindingReq.LoadBalancerId = ucloud.String(cloudLoadbalancerId) deleteSSLBindingReq.ListenerId = ucloud.String(cloudListenerId) deleteSSLBindingReq.SSLIds = sslIdsToDelete deleteSSLBindingResp, err := d.sdkClient.DeleteSSLBinding(deleteSSLBindingReq) d.logger.Debug("sdk request 'ulb.DeleteSSLBinding'", slog.Any("request", deleteSSLBindingReq), slog.Any("response", deleteSSLBindingResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'ulb.DeleteSSLBinding': %w", err) } } } return nil } func createSDKClient(privateKey, publicKey, projectId, region string) (*ucloudsdk.ULBClient, error) { if privateKey == "" { return nil, fmt.Errorf("ucloud: invalid private key") } if publicKey == "" { return nil, fmt.Errorf("ucloud: invalid public key") } cfg := ucloud.NewConfig() cfg.ProjectId = projectId cfg.Region = region credential := auth.NewCredential() credential.PrivateKey = privateKey credential.PublicKey = publicKey client := ucloudsdk.NewClient(&cfg, &credential) return client, nil } ================================================ FILE: pkg/core/deployer/providers/ucloud-ualb/ucloud_ualb_test.go ================================================ package ucloudualb_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-ualb" ) var ( fInputCertPath string fInputKeyPath string fPrivateKey string fPublicKey string fRegion string fLoadbalancerId string fListenerId string ) func init() { argsPrefix := "UCLOUDUALB_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fPrivateKey, argsPrefix+"PRIVATEKEY", "", "") flag.StringVar(&fPublicKey, argsPrefix+"PUBLICKEY", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "") flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "") } /* Shell command to run this test: go test -v ./ucloud_ualb_test.go -args \ --UCLOUDUALB_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --UCLOUDUALB_INPUTKEYPATH="/path/to/your-input-key.pem" \ --UCLOUDUALB_PRIVATEKEY="your-private-key" \ --UCLOUDUALB_PUBLICKEY="your-public-key" \ --UCLOUDUALB_REGION="cn-bj2" \ --UCLOUDUALB_LOADBALANCERID="your-loadbalancer-id" \ --UCLOUDUALB_LISTENERID="your-listener-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("PRIVATEKEY: %v", fPrivateKey), fmt.Sprintf("PUBLICKEY: %v", fPublicKey), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId), fmt.Sprintf("LISTENERID: %v", fListenerId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ PrivateKey: fPrivateKey, PublicKey: fPublicKey, Region: fRegion, ResourceType: provider.RESOURCE_TYPE_LISTENER, LoadbalancerId: fLoadbalancerId, ListenerId: fListenerId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/ucloud-ucdn/ucloud_ucdn.go ================================================ package uclouducdn import ( "context" "errors" "fmt" "log/slog" "strconv" "github.com/ucloud/ucloud-sdk-go/ucloud" "github.com/ucloud/ucloud-sdk-go/ucloud/auth" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ucloud-ussl" "github.com/certimate-go/certimate/pkg/core/deployer" ucloudsdk "github.com/certimate-go/certimate/pkg/sdk3rd/ucloud/ucdn" ) type DeployerConfig struct { // 优刻得 API 私钥。 PrivateKey string `json:"privateKey"` // 优刻得 API 公钥。 PublicKey string `json:"publicKey"` // 优刻得项目 ID。 ProjectId string `json:"projectId,omitempty"` // 加速域名 ID。 DomainId string `json:"domainId"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *ucloudsdk.UCDNClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.PrivateKey, config.PublicKey, config.ProjectId) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ PrivateKey: config.PrivateKey, PublicKey: config.PublicKey, ProjectId: config.ProjectId, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { if d.config.DomainId == "" { return nil, errors.New("config `domainId` is required") } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取加速域名配置 // REF: https://docs.ucloud.cn/api/ucdn-api/get_ucdn_domain_config getUcdnDomainConfigReq := d.sdkClient.NewGetUcdnDomainConfigRequest() getUcdnDomainConfigReq.DomainId = []string{d.config.DomainId} getUcdnDomainConfigResp, err := d.sdkClient.GetUcdnDomainConfig(getUcdnDomainConfigReq) d.logger.Debug("sdk request 'ucdn.GetUcdnDomainConfig'", slog.Any("request", getUcdnDomainConfigReq), slog.Any("response", getUcdnDomainConfigResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'ucdn.GetUcdnDomainConfig': %w", err) } else if len(getUcdnDomainConfigResp.DomainList) == 0 { return nil, fmt.Errorf("could not find domain '%s'", d.config.DomainId) } // 更新 HTTPS 加速配置 // REF: https://docs.ucloud.cn/api/ucdn-api/update_ucdn_domain_https_config_v2 certId, _ := strconv.Atoi(upres.CertId) updateUcdnDomainHttpsConfigV2Req := d.sdkClient.NewUpdateUcdnDomainHttpsConfigV2Request() updateUcdnDomainHttpsConfigV2Req.DomainId = ucloud.String(d.config.DomainId) updateUcdnDomainHttpsConfigV2Req.HttpsStatusCn = ucloud.String(getUcdnDomainConfigResp.DomainList[0].HttpsStatusCn) updateUcdnDomainHttpsConfigV2Req.HttpsStatusAbroad = ucloud.String(getUcdnDomainConfigResp.DomainList[0].HttpsStatusAbroad) updateUcdnDomainHttpsConfigV2Req.HttpsStatusAbroad = ucloud.String(getUcdnDomainConfigResp.DomainList[0].HttpsStatusAbroad) updateUcdnDomainHttpsConfigV2Req.CertId = ucloud.Int(certId) updateUcdnDomainHttpsConfigV2Req.CertName = ucloud.String(upres.CertName) updateUcdnDomainHttpsConfigV2Req.CertType = ucloud.String("ussl") updateUcdnDomainHttpsConfigV2Resp, err := d.sdkClient.UpdateUcdnDomainHttpsConfigV2(updateUcdnDomainHttpsConfigV2Req) d.logger.Debug("sdk request 'ucdn.UpdateUcdnDomainHttpsConfigV2'", slog.Any("request", updateUcdnDomainHttpsConfigV2Req), slog.Any("response", updateUcdnDomainHttpsConfigV2Resp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'ucdn.UpdateUcdnDomainHttpsConfigV2': %w", err) } return &deployer.DeployResult{}, nil } func createSDKClient(privateKey, publicKey, projectId string) (*ucloudsdk.UCDNClient, error) { if privateKey == "" { return nil, fmt.Errorf("ucloud: invalid private key") } if publicKey == "" { return nil, fmt.Errorf("ucloud: invalid public key") } cfg := ucloud.NewConfig() cfg.ProjectId = projectId credential := auth.NewCredential() credential.PrivateKey = privateKey credential.PublicKey = publicKey client := ucloudsdk.NewClient(&cfg, &credential) return client, nil } ================================================ FILE: pkg/core/deployer/providers/ucloud-ucdn/ucloud_ucdn_test.go ================================================ package uclouducdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-ucdn" ) var ( fInputCertPath string fInputKeyPath string fPrivateKey string fPublicKey string fDomainId string ) func init() { argsPrefix := "UCLOUDUCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fPrivateKey, argsPrefix+"PRIVATEKEY", "", "") flag.StringVar(&fPublicKey, argsPrefix+"PUBLICKEY", "", "") flag.StringVar(&fDomainId, argsPrefix+"DOMAINID", "", "") } /* Shell command to run this test: go test -v ./ucloud_ucdn_test.go -args \ --UCLOUDUCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --UCLOUDUCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --UCLOUDUCDN_PRIVATEKEY="your-private-key" \ --UCLOUDUCDN_PUBLICKEY="your-public-key" \ --UCLOUDUCDN_DOMAINID="your-domain-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("PRIVATEKEY: %v", fPrivateKey), fmt.Sprintf("PUBLICKEY: %v", fPublicKey), fmt.Sprintf("DOMAIN: %v", fDomainId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ PrivateKey: fPrivateKey, PublicKey: fPublicKey, DomainId: fDomainId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/ucloud-uclb/consts.go ================================================ package uclouduclb const ( // 资源类型:部署到指定负载均衡器。 RESOURCE_TYPE_LOADBALANCER = "loadbalancer" // 资源类型:部署到指定 VServer。 RESOURCE_TYPE_VSERVER = "vserver" ) ================================================ FILE: pkg/core/deployer/providers/ucloud-uclb/ucloud_uclb.go ================================================ package uclouduclb import ( "context" "errors" "fmt" "log/slog" "sync" "github.com/samber/lo" "github.com/ucloud/ucloud-sdk-go/services/ulb" "github.com/ucloud/ucloud-sdk-go/ucloud" "github.com/ucloud/ucloud-sdk-go/ucloud/auth" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ucloud-ulb" "github.com/certimate-go/certimate/pkg/core/deployer" ucloudsdk "github.com/certimate-go/certimate/pkg/sdk3rd/ucloud/ulb" ) type DeployerConfig struct { // 优刻得 API 私钥。 PrivateKey string `json:"privateKey"` // 优刻得 API 公钥。 PublicKey string `json:"publicKey"` // 优刻得项目 ID。 ProjectId string `json:"projectId,omitempty"` // 优刻得地域。 Region string `json:"region"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 负载均衡实例 ID。 // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_VSERVER] 时必填。 LoadbalancerId string `json:"loadbalancerId,omitempty"` // 负载均衡 VServer ID。 // 部署资源类型为 [RESOURCE_TYPE_VSERVER] 时必填。 VServerId string `json:"vserverId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *ucloudsdk.ULBClient sdkCertmgr certmgr.Provider sslId2PemMap map[string]string sslId2PemMapMu sync.Mutex } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.PrivateKey, config.PublicKey, config.ProjectId, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ PrivateKey: config.PrivateKey, PublicKey: config.PublicKey, ProjectId: config.ProjectId, Region: config.Region, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, sslId2PemMap: make(map[string]string), sslId2PemMapMu: sync.Mutex{}, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) d.sslId2PemMapMu.Lock() d.sslId2PemMap[upres.CertId] = certPEM d.sslId2PemMapMu.Unlock() } // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_LOADBALANCER: if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil { return nil, err } case RESOURCE_TYPE_VSERVER: if err := d.deployToVServer(ctx, upres.CertId); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error { if d.config.LoadbalancerId == "" { return errors.New("config `loadbalancerId` is required") } // 获取 CLB 下的 HTTPS VServer 列表 // REF: https://docs.ucloud.cn/api/ulb-api/describe_vserver vserverIds := make([]string, 0) describeVServerOffset := 0 describeVServerLimit := 100 for { select { case <-ctx.Done(): return ctx.Err() default: } describeVServerReq := d.sdkClient.NewDescribeVServerRequest() describeVServerReq.ULBId = ucloud.String(d.config.LoadbalancerId) describeVServerReq.Offset = ucloud.Int(describeVServerOffset) describeVServerReq.Limit = ucloud.Int(describeVServerLimit) describeVServerResp, err := d.sdkClient.DescribeVServer(describeVServerReq) d.logger.Debug("sdk request 'ulb.DescribeVServer'", slog.Any("request", describeVServerReq), slog.Any("response", describeVServerResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'ulb.DescribeVServer': %w", err) } for _, vserverItem := range describeVServerResp.DataSet { if vserverItem.Protocol == "HTTPS" { vserverIds = append(vserverIds, vserverItem.VServerId) } } if len(describeVServerResp.DataSet) < describeVServerLimit { break } describeVServerOffset += describeVServerLimit } // 遍历更新 VServer 证书 if len(vserverIds) == 0 { d.logger.Info("no clb vservers to deploy") } else { d.logger.Info("found https vservers to deploy", slog.Any("vserverIds", vserverIds)) var errs []error for _, vserverId := range vserverIds { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateVServerCertificate(ctx, d.config.LoadbalancerId, vserverId, cloudCertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) deployToVServer(ctx context.Context, cloudCertId string) error { if d.config.LoadbalancerId == "" { return errors.New("config `loadbalancerId` is required") } if d.config.VServerId == "" { return errors.New("config `vserverId` is required") } if err := d.updateVServerCertificate(ctx, d.config.LoadbalancerId, d.config.VServerId, cloudCertId); err != nil { return err } return nil } func (d *Deployer) updateVServerCertificate(ctx context.Context, cloudLoadbalancerId, cloudVServerId string, cloudCertId string) error { // 获取 CLB 下的 VServer 信息 // REF: https://docs.ucloud.cn/api/ulb-api/describe_vserver describeVServerReq := d.sdkClient.NewDescribeVServerRequest() describeVServerReq.ULBId = ucloud.String(cloudLoadbalancerId) describeVServerReq.VServerId = ucloud.String(cloudVServerId) describeVServerReq.Limit = ucloud.Int(1) describeVServerResp, err := d.sdkClient.DescribeVServer(describeVServerReq) d.logger.Debug("sdk request 'ulb.DescribeVServer'", slog.Any("request", describeVServerReq), slog.Any("response", describeVServerResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'ulb.DescribeVServer': %w", err) } else if len(describeVServerResp.DataSet) == 0 { return fmt.Errorf("could not find vserver '%s'", cloudVServerId) } // 跳过已部署过的 VServer vserverInfo := describeVServerResp.DataSet[0] if lo.ContainsBy(vserverInfo.SSLSet, func(item ulb.ULBSSLSet) bool { return item.SSLId == cloudCertId }) { return nil } // 解绑 SSL 证书 // REF: https://docs.ucloud.cn/api/ulb-api/unbind_ssl // // 注意,虽然文档中描述为数组结构,但实际 VServer 最多只允许绑定一个证书,因此需要先解绑旧证书才能绑定新证书 // https://github.com/certimate-go/certimate/issues/1224 for _, sslItem := range vserverInfo.SSLSet { unbindSSLReq := d.sdkClient.NewUnbindSSLRequest() unbindSSLReq.ULBId = ucloud.String(cloudLoadbalancerId) unbindSSLReq.VServerId = ucloud.String(cloudVServerId) unbindSSLReq.SSLId = ucloud.String(sslItem.SSLId) unbindSSLResp, err := d.sdkClient.UnbindSSL(unbindSSLReq) d.logger.Debug("sdk request 'ulb.UnbindSSL'", slog.Any("request", unbindSSLReq), slog.Any("response", unbindSSLResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'ulb.UnbindSSL': %w", err) } } // 绑定 SSL 证书 // REF: https://docs.ucloud.cn/api/ulb-api/bind_ssl bindSSLReq := d.sdkClient.NewBindSSLRequest() bindSSLReq.ULBId = ucloud.String(cloudLoadbalancerId) bindSSLReq.VServerId = ucloud.String(cloudVServerId) bindSSLReq.SSLId = ucloud.String(cloudCertId) bindSSLResp, err := d.sdkClient.BindSSL(bindSSLReq) d.logger.Debug("sdk request 'ulb.BindSSL'", slog.Any("request", bindSSLReq), slog.Any("response", bindSSLResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'ulb.BindSSL': %w", err) } return nil } func createSDKClient(privateKey, publicKey, projectId, region string) (*ucloudsdk.ULBClient, error) { if privateKey == "" { return nil, fmt.Errorf("ucloud: invalid private key") } if publicKey == "" { return nil, fmt.Errorf("ucloud: invalid public key") } cfg := ucloud.NewConfig() cfg.ProjectId = projectId cfg.Region = region credential := auth.NewCredential() credential.PrivateKey = privateKey credential.PublicKey = publicKey client := ucloudsdk.NewClient(&cfg, &credential) return client, nil } ================================================ FILE: pkg/core/deployer/providers/ucloud-uclb/ucloud_uclb_test.go ================================================ package uclouduclb_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-uclb" ) var ( fInputCertPath string fInputKeyPath string fPrivateKey string fPublicKey string fRegion string fLoadbalancerId string fVServerId string ) func init() { argsPrefix := "UCLOUDUCLB_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fPrivateKey, argsPrefix+"PRIVATEKEY", "", "") flag.StringVar(&fPublicKey, argsPrefix+"PUBLICKEY", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "") flag.StringVar(&fVServerId, argsPrefix+"VSERVERID", "", "") } /* Shell command to run this test: go test -v ./ucloud_uclb_test.go -args \ --UCLOUDUCLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --UCLOUDUCLB_INPUTKEYPATH="/path/to/your-input-key.pem" \ --UCLOUDUCLB_PRIVATEKEY="your-private-key" \ --UCLOUDUCLB_PUBLICKEY="your-public-key" \ --UCLOUDUCLB_REGION="cn-bj2" \ --UCLOUDUCLB_LOADBALANCERID="your-loadbalancer-id" \ --UCLOUDUCLB_VSERVERID="your-vserver-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("PRIVATEKEY: %v", fPrivateKey), fmt.Sprintf("PUBLICKEY: %v", fPublicKey), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId), fmt.Sprintf("VSERVERID: %v", fVServerId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ PrivateKey: fPrivateKey, PublicKey: fPublicKey, Region: fRegion, ResourceType: provider.RESOURCE_TYPE_VSERVER, LoadbalancerId: fLoadbalancerId, VServerId: fVServerId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/ucloud-uewaf/ucloud_uewaf.go ================================================ package uclouduewaf import ( "context" "crypto/md5" "encoding/base64" "encoding/hex" "errors" "fmt" "log/slog" "time" "github.com/ucloud/ucloud-sdk-go/ucloud" "github.com/ucloud/ucloud-sdk-go/ucloud/auth" "github.com/certimate-go/certimate/pkg/core/deployer" ucloudsdk "github.com/certimate-go/certimate/pkg/sdk3rd/ucloud/uewaf" ) type DeployerConfig struct { // 优刻得 API 私钥。 PrivateKey string `json:"privateKey"` // 优刻得 API 公钥。 PublicKey string `json:"publicKey"` // 优刻得项目 ID。 ProjectId string `json:"projectId,omitempty"` // 自定义域名(不支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *ucloudsdk.UEWAFClient } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.PrivateKey, config.PublicKey, config.ProjectId) 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) (*deployer.DeployResult, error) { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } // 生成优刻得所需的证书参数 certPEMBase64 := base64.StdEncoding.EncodeToString([]byte(certPEM)) privkeyPEMBase64 := base64.StdEncoding.EncodeToString([]byte(privkeyPEM)) certMd5 := md5.Sum([]byte(certPEMBase64 + privkeyPEMBase64)) certMd5Hex := hex.EncodeToString(certMd5[:]) certName := fmt.Sprintf("certimate_%d", time.Now().UnixMilli()) // 添加 SSL 证书 // REF: https://docs.ucloud.cn/api/uewaf-api/add_waf_domain_certificate_info addWafDomainCertificateInfoReq := d.sdkClient.NewAddWafDomainCertificateInfoRequest() addWafDomainCertificateInfoReq.Domain = ucloud.String(d.config.Domain) addWafDomainCertificateInfoReq.CertificateName = ucloud.String(certName) addWafDomainCertificateInfoReq.SslPublicKey = ucloud.String(certPEMBase64) addWafDomainCertificateInfoReq.SslPrivateKey = ucloud.String(privkeyPEMBase64) addWafDomainCertificateInfoReq.SslMD = ucloud.String(certMd5Hex) addWafDomainCertificateInfoResp, err := d.sdkClient.AddWafDomainCertificateInfo(addWafDomainCertificateInfoReq) d.logger.Debug("sdk request 'uewaf.AddWafDomainCertificateInfo'", slog.Any("request", addWafDomainCertificateInfoReq), slog.Any("response", addWafDomainCertificateInfoResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'uewaf.AddWafDomainCertificateInfo': %w", err) } return &deployer.DeployResult{}, nil } func createSDKClient(privateKey, publicKey, projectId string) (*ucloudsdk.UEWAFClient, error) { if privateKey == "" { return nil, fmt.Errorf("ucloud: invalid private key") } if publicKey == "" { return nil, fmt.Errorf("ucloud: invalid public key") } cfg := ucloud.NewConfig() cfg.ProjectId = projectId credential := auth.NewCredential() credential.PrivateKey = privateKey credential.PublicKey = publicKey client := ucloudsdk.NewClient(&cfg, &credential) return client, nil } ================================================ FILE: pkg/core/deployer/providers/ucloud-uewaf/ucloud_uewaf_test.go ================================================ package uclouduewaf_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-uewaf" ) var ( fInputCertPath string fInputKeyPath string fPrivateKey string fPublicKey string fDomain string ) func init() { argsPrefix := "UCLOUDUEWAF_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fPrivateKey, argsPrefix+"PRIVATEKEY", "", "") flag.StringVar(&fPublicKey, argsPrefix+"PUBLICKEY", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./ucloud_uewaf_test.go -args \ --UCLOUDUEWAF_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --UCLOUDUEWAF_INPUTKEYPATH="/path/to/your-input-key.pem" \ --UCLOUDUEWAF_PRIVATEKEY="your-private-key" \ --UCLOUDUEWAF_PUBLICKEY="your-public-key" \ --UCLOUDUEWAF_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("PRIVATEKEY: %v", fPrivateKey), fmt.Sprintf("PUBLICKEY: %v", fPublicKey), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ PrivateKey: fPrivateKey, PublicKey: fPublicKey, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/ucloud-upathx/ucloud_upathx.go ================================================ package ucloudupathx import ( "context" "errors" "fmt" "log/slog" "github.com/ucloud/ucloud-sdk-go/services/uaccount" "github.com/ucloud/ucloud-sdk-go/ucloud" "github.com/ucloud/ucloud-sdk-go/ucloud/auth" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ucloud-upathx" "github.com/certimate-go/certimate/pkg/core/deployer" ucloudsdk "github.com/certimate-go/certimate/pkg/sdk3rd/ucloud/upathx" ) type DeployerConfig struct { // 优刻得 API 私钥。 PrivateKey string `json:"privateKey"` // 优刻得 API 公钥。 PublicKey string `json:"publicKey"` // 优刻得项目 ID。 ProjectId string `json:"projectId,omitempty"` // 加速器实例 ID。 AcceleratorId string `json:"acceleratorId"` // 加速器监听端口。 ListenerPort int32 `json:"listenerPort"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *ucloudsdk.UPathXClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.PrivateKey, config.PublicKey, config.ProjectId) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ PrivateKey: config.PrivateKey, PublicKey: config.PublicKey, ProjectId: config.ProjectId, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { if d.config.AcceleratorId == "" { return nil, errors.New("config `acceleratorId` is required") } if d.config.ListenerPort == 0 { return nil, errors.New("config `listenerPort` is required") } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 绑定 PathX SSL 证书 // REF: https://docs.ucloud.cn/api/pathx-api/bind_path_xssl bindPathXSSLReq := d.sdkClient.NewBindPathXSSLRequest() bindPathXSSLReq.UGAId = ucloud.String(d.config.AcceleratorId) bindPathXSSLReq.Port = []int{int(d.config.ListenerPort)} bindPathXSSLReq.SSLId = ucloud.String(upres.CertId) bindPathXSSLResp, err := d.sdkClient.BindPathXSSL(bindPathXSSLReq) d.logger.Debug("sdk request 'pathx.BindPathXSSL'", slog.Any("request", bindPathXSSLReq), slog.Any("response", bindPathXSSLResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'pathx.BindPathXSSL': %w", err) } return &deployer.DeployResult{}, nil } func createSDKClient(privateKey, publicKey, projectId string) (*ucloudsdk.UPathXClient, error) { if privateKey == "" { return nil, fmt.Errorf("ucloud: invalid private key") } if publicKey == "" { return nil, fmt.Errorf("ucloud: invalid public key") } cfg := ucloud.NewConfig() cfg.ProjectId = projectId // PathX 相关接口要求必传 ProjectId 参数 if cfg.ProjectId == "" { defaultProjectId, err := getSDKDefaultProjectId(privateKey, publicKey) if err != nil { return nil, err } cfg.ProjectId = defaultProjectId } credential := auth.NewCredential() credential.PrivateKey = privateKey credential.PublicKey = publicKey client := ucloudsdk.NewClient(&cfg, &credential) return client, nil } func getSDKDefaultProjectId(privateKey, publicKey string) (string, error) { cfg := ucloud.NewConfig() credential := auth.NewCredential() credential.PrivateKey = privateKey credential.PublicKey = publicKey client := uaccount.NewClient(&cfg, &credential) request := client.NewGetProjectListRequest() response, err := client.GetProjectList(request) if err != nil { return "", err } for _, projectItem := range response.ProjectSet { if projectItem.IsDefault { return projectItem.ProjectId, nil } } return "", errors.New("ucloud: no default project found") } ================================================ FILE: pkg/core/deployer/providers/ucloud-upathx/ucloud_upathx_test.go ================================================ package ucloudupathx_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-upathx" ) var ( fInputCertPath string fInputKeyPath string fPrivateKey string fPublicKey string fRegion string fAcceleratorId string fListenerPort int ) func init() { argsPrefix := "UCLOUDUPATHX_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fPrivateKey, argsPrefix+"PRIVATEKEY", "", "") flag.StringVar(&fPublicKey, argsPrefix+"PUBLICKEY", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fAcceleratorId, argsPrefix+"ACCELERATORID", "", "") flag.IntVar(&fListenerPort, argsPrefix+"LISTENERPORT", 443, "") } /* Shell command to run this test: go test -v ./ucloud_upathx_test.go -args \ --UCLOUDUPATHX_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --UCLOUDUPATHX_INPUTKEYPATH="/path/to/your-input-key.pem" \ --UCLOUDUPATHX_PRIVATEKEY="your-private-key" \ --UCLOUDUPATHX_PUBLICKEY="your-public-key" \ --UCLOUDUPATHX_ACCELERATORID="your-uga-id" \ --UCLOUDUPATHX_ACCELERATORPORT="443" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("PRIVATEKEY: %v", fPrivateKey), fmt.Sprintf("PUBLICKEY: %v", fPublicKey), fmt.Sprintf("ACCELERATORID: %v", fAcceleratorId), fmt.Sprintf("LISTENERPORT: %v", fListenerPort), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ PrivateKey: fPrivateKey, PublicKey: fPublicKey, AcceleratorId: fAcceleratorId, ListenerPort: int32(fListenerPort), }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/ucloud-us3/ucloud_us3.go ================================================ package ucloudus3 import ( "context" "errors" "fmt" "log/slog" "github.com/ucloud/ucloud-sdk-go/ucloud" "github.com/ucloud/ucloud-sdk-go/ucloud/auth" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ucloud-ussl" "github.com/certimate-go/certimate/pkg/core/deployer" ucloudsdk "github.com/certimate-go/certimate/pkg/sdk3rd/ucloud/ufile" ) type DeployerConfig struct { // 优刻得 API 私钥。 PrivateKey string `json:"privateKey"` // 优刻得 API 公钥。 PublicKey string `json:"publicKey"` // 优刻得项目 ID。 ProjectId string `json:"projectId,omitempty"` // 优刻得地域。 Region string `json:"region"` // 存储桶名。 Bucket string `json:"bucket"` // 自定义域名(不支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *ucloudsdk.UFileClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.PrivateKey, config.PublicKey, config.ProjectId, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ PrivateKey: config.PrivateKey, PublicKey: config.PublicKey, ProjectId: config.ProjectId, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { if d.config.Bucket == "" { return nil, errors.New("config `bucket` is required") } if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 添加 SSL 证书 // REF: https://docs.ucloud.cn/api/ufile-api/add_ufile_ssl_cert addUFileSSLCertReq := d.sdkClient.NewAddUFileSSLCertRequest() addUFileSSLCertReq.BucketName = ucloud.String(d.config.Bucket) addUFileSSLCertReq.Domain = ucloud.String(d.config.Domain) addUFileSSLCertReq.USSLId = ucloud.String(upres.CertId) addUFileSSLCertReq.CertificateName = ucloud.String(upres.CertName) addUFileSSLCertResp, err := d.sdkClient.AddUFileSSLCert(addUFileSSLCertReq) d.logger.Debug("sdk request 'us3.AddUFileSSLCert'", slog.Any("request", addUFileSSLCertReq), slog.Any("response", addUFileSSLCertResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'us3.AddUFileSSLCert': %w", err) } return &deployer.DeployResult{}, nil } func createSDKClient(privateKey, publicKey, projectId, region string) (*ucloudsdk.UFileClient, error) { if privateKey == "" { return nil, fmt.Errorf("ucloud: invalid private key") } if publicKey == "" { return nil, fmt.Errorf("ucloud: invalid public key") } cfg := ucloud.NewConfig() cfg.ProjectId = projectId cfg.Region = region credential := auth.NewCredential() credential.PrivateKey = privateKey credential.PublicKey = publicKey client := ucloudsdk.NewClient(&cfg, &credential) return client, nil } ================================================ FILE: pkg/core/deployer/providers/ucloud-us3/ucloud_us3_test.go ================================================ package ucloudus3_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/ucloud-us3" ) var ( fInputCertPath string fInputKeyPath string fPrivateKey string fPublicKey string fRegion string fBucket string fDomain string ) func init() { argsPrefix := "UCLOUDUS3_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fPrivateKey, argsPrefix+"PRIVATEKEY", "", "") flag.StringVar(&fPublicKey, argsPrefix+"PUBLICKEY", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fBucket, argsPrefix+"BUCKET", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./ucloud_us3_test.go -args \ --UCLOUDUS3_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --UCLOUDUS3_INPUTKEYPATH="/path/to/your-input-key.pem" \ --UCLOUDUS3_PRIVATEKEY="your-private-key" \ --UCLOUDUS3_PUBLICKEY="your-public-key" \ --UCLOUDUS3_REGION="cn-bj2" \ --UCLOUDUS3_BUCKET="your-us3-bucket" \ --UCLOUDUS3_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("PRIVATEKEY: %v", fPrivateKey), fmt.Sprintf("PUBLICKEY: %v", fPublicKey), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("BUCKET: %v", fBucket), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ PrivateKey: fPrivateKey, PublicKey: fPublicKey, Region: fRegion, Bucket: fBucket, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/unicloud-webhost/unicloud_webhost.go ================================================ package unicloudwebhost import ( "context" "errors" "fmt" "log/slog" "net/url" "github.com/certimate-go/certimate/pkg/core/deployer" unicloudsdk "github.com/certimate-go/certimate/pkg/sdk3rd/dcloud/unicloud" ) type DeployerConfig struct { // uniCloud 控制台账号。 Username string `json:"username"` // uniCloud 控制台密码。 Password string `json:"password"` // 服务空间提供商。 // 可取值 "aliyun"、"tencent"。 SpaceProvider string `json:"spaceProvider"` // 服务空间 ID。 SpaceId string `json:"spaceId"` // 托管网站域名(不支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *unicloudsdk.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.Username, config.Password) 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) (*deployer.DeployResult, error) { if d.config.SpaceProvider == "" { return nil, errors.New("config `spaceProvider` is required") } if d.config.SpaceId == "" { return nil, errors.New("config `spaceId` is required") } if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } // 变更网站证书 createDomainWithCertReq := &unicloudsdk.CreateDomainWithCertRequest{ Provider: d.config.SpaceProvider, SpaceId: d.config.SpaceId, Domain: d.config.Domain, Cert: url.QueryEscape(certPEM), Key: url.QueryEscape(privkeyPEM), } createDomainWithCertResp, err := d.sdkClient.CreateDomainWithCert(createDomainWithCertReq) d.logger.Debug("sdk request 'unicloud.host.CreateDomainWithCert'", slog.Any("request", createDomainWithCertReq), slog.Any("response", createDomainWithCertResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'unicloud.host.CreateDomainWithCert': %w", err) } return &deployer.DeployResult{}, nil } func createSDKClient(username, password string) (*unicloudsdk.Client, error) { return unicloudsdk.NewClient(username, password) } ================================================ FILE: pkg/core/deployer/providers/unicloud-webhost/unicloud_webhost_test.go ================================================ package unicloudwebhost_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/unicloud-webhost" ) var ( fInputCertPath string fInputKeyPath string fUsername string fPassword string fSpaceProvider string fSpaceId string fDomain string ) func init() { argsPrefix := "UNICLOUDWEBHOST_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "") flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "") flag.StringVar(&fSpaceProvider, argsPrefix+"SPACEPROVIDER", "", "") flag.StringVar(&fSpaceId, argsPrefix+"SPACEID", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./unicloud_webhost_test.go -args \ --UNICLOUDWEBHOST_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --UNICLOUDWEBHOST_INPUTKEYPATH="/path/to/your-input-key.pem" \ --UNICLOUDWEBHOST_USERNAME="your-username" \ --UNICLOUDWEBHOST_PASSWORD="your-password" \ --UNICLOUDWEBHOST_SPACEPROVIDER="aliyun/tencent" \ --UNICLOUDWEBHOST_SPACEID="your-space-id" \ --UNICLOUDWEBHOST_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("USERNAME: %v", fUsername), fmt.Sprintf("PASSWORD: %v", fPassword), fmt.Sprintf("SPACEPROVIDER: %v", fSpaceProvider), fmt.Sprintf("SPACEID: %v", fSpaceId), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ Username: fUsername, Password: fPassword, SpaceProvider: fSpaceProvider, SpaceId: fSpaceId, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/upyun-cdn/consts.go ================================================ package upyuncdn const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/upyun-cdn/upyun_cdn.go ================================================ package upyuncdn import ( "context" "errors" "fmt" "log/slog" "strings" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/upyun-ssl" "github.com/certimate-go/certimate/pkg/core/deployer" upyunsdk "github.com/certimate-go/certimate/pkg/sdk3rd/upyun/console" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 又拍云账号用户名。 Username string `json:"username"` // 又拍云账号密码。 Password string `json:"password"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *upyunsdk.Client sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.Username, config.Password) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ Username: config.Username, Password: config.Password, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名列表 var domains []string switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return xcerthostname.IsMatch(d.config.Domain, domain) }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by wildcard") } } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no cdn domains to deploy") } else { d.logger.Info("found cdn domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 获取服务列表 getBucketsPage := 1 getBucketsPerPage := 10 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } getBucketsReq := &upyunsdk.GetBucketsRequest{ Type: "ucdn", Tag: "all", Status: "all", IsSecurityCDN: false, WithDomains: true, Page: int32(getBucketsPage), PerPage: int32(getBucketsPerPage), } getBucketsResp, err := d.sdkClient.GetBucketsWithContext(ctx, getBucketsReq) d.logger.Debug("sdk request 'console.GetBuckets'", slog.Any("request", getBucketsReq), slog.Any("response", getBucketsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'console.GetBuckets': %w", err) } if getBucketsResp.Data == nil { break } for _, bucketItem := range getBucketsResp.Data.Buckets { if !bucketItem.Visible { continue } for _, domainItem := range bucketItem.Domains { if strings.EqualFold(domainItem.Status, "NORMAL") && !strings.HasSuffix(domainItem.Domain, ".test.upcdn.net") { domains = append(domains, domainItem.Domain) } } } if len(getBucketsResp.Data.Buckets) < getBucketsPerPage { break } getBucketsPage++ } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error { // 获取域名证书配置 getHttpsServiceManagerResp, err := d.sdkClient.GetHttpsServiceManagerWithContext(ctx, domain) d.logger.Debug("sdk request 'console.GetHttpsServiceManager'", slog.String("request.domain", domain), slog.Any("response", getHttpsServiceManagerResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'console.GetHttpsServiceManager': %w", err) } // 判断域名是否已启用 HTTPS // 如果已启用,迁移域名证书;否则,设置新证书 _, lastCertIndex, _ := lo.FindIndexOf(getHttpsServiceManagerResp.Data.Domains, func(item upyunsdk.HttpsServiceManagerDomain) bool { return item.Https }) if lastCertIndex == -1 { updateHttpsCertificateManagerReq := &upyunsdk.UpdateHttpsCertificateManagerRequest{ CertificateId: cloudCertId, Domain: domain, Https: true, ForceHttps: true, } updateHttpsCertificateManagerResp, err := d.sdkClient.UpdateHttpsCertificateManagerWithContext(ctx, updateHttpsCertificateManagerReq) d.logger.Debug("sdk request 'console.EnableDomainHttps'", slog.Any("request", updateHttpsCertificateManagerReq), slog.Any("response", updateHttpsCertificateManagerResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'console.UpdateHttpsCertificateManager': %w", err) } } else if getHttpsServiceManagerResp.Data.Domains[lastCertIndex].CertificateId != cloudCertId { migrateHttpsDomainReq := &upyunsdk.MigrateHttpsDomainRequest{ CertificateId: cloudCertId, Domain: domain, } migrateHttpsDomainResp, err := d.sdkClient.MigrateHttpsDomainWithContext(ctx, migrateHttpsDomainReq) d.logger.Debug("sdk request 'console.MigrateHttpsDomain'", slog.Any("request", migrateHttpsDomainReq), slog.Any("response", migrateHttpsDomainResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'console.MigrateHttpsDomain': %w", err) } } return nil } func createSDKClient(username, password string) (*upyunsdk.Client, error) { return upyunsdk.NewClient(username, password) } ================================================ FILE: pkg/core/deployer/providers/upyun-cdn/upyun_cdn_test.go ================================================ package upyuncdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/upyun-cdn" ) var ( fInputCertPath string fInputKeyPath string fUsername string fPassword string fDomain string ) func init() { argsPrefix := "UPYUNCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "") flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./upyun_cdn_test.go -args \ --UPYUNCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --UPYUNCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --UPYUNCDN_USERNAME="your-username" \ --UPYUNCDN_PASSWORD="your-password" \ --UPYUNCDN_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("USERNAME: %v", fUsername), fmt.Sprintf("PASSWORD: %v", fPassword), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ Username: fUsername, Password: fPassword, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/upyun-file/upyun_file.go ================================================ package upyunfile import ( "context" "errors" "fmt" "log/slog" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/upyun-ssl" "github.com/certimate-go/certimate/pkg/core/deployer" upyunsdk "github.com/certimate-go/certimate/pkg/sdk3rd/upyun/console" ) type DeployerConfig struct { // 又拍云账号用户名。 Username string `json:"username"` // 又拍云账号密码。 Password string `json:"password"` // 存储桶名。暂时无用。 Bucket string `json:"bucket"` // 自定义域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *upyunsdk.Client sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.Username, config.Password) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ Username: config.Username, Password: config.Password, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取域名证书配置 getHttpsServiceManagerResp, err := d.sdkClient.GetHttpsServiceManagerWithContext(ctx, d.config.Domain) d.logger.Debug("sdk request 'console.GetHttpsServiceManager'", slog.String("request.domain", d.config.Domain), slog.Any("response", getHttpsServiceManagerResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'console.GetHttpsServiceManager': %w", err) } // 判断域名是否已启用 HTTPS // 如果已启用,迁移域名证书;否则,设置新证书 _, lastCertIndex, _ := lo.FindIndexOf(getHttpsServiceManagerResp.Data.Domains, func(item upyunsdk.HttpsServiceManagerDomain) bool { return item.Https }) if lastCertIndex == -1 { updateHttpsCertificateManagerReq := &upyunsdk.UpdateHttpsCertificateManagerRequest{ CertificateId: upres.CertId, Domain: d.config.Domain, Https: true, ForceHttps: true, } updateHttpsCertificateManagerResp, err := d.sdkClient.UpdateHttpsCertificateManagerWithContext(ctx, updateHttpsCertificateManagerReq) d.logger.Debug("sdk request 'console.EnableDomainHttps'", slog.Any("request", updateHttpsCertificateManagerReq), slog.Any("response", updateHttpsCertificateManagerResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'console.UpdateHttpsCertificateManager': %w", err) } } else if getHttpsServiceManagerResp.Data.Domains[lastCertIndex].CertificateId != upres.CertId { migrateHttpsDomainReq := &upyunsdk.MigrateHttpsDomainRequest{ CertificateId: upres.CertId, Domain: d.config.Domain, } migrateHttpsDomainResp, err := d.sdkClient.MigrateHttpsDomainWithContext(ctx, migrateHttpsDomainReq) d.logger.Debug("sdk request 'console.MigrateHttpsDomain'", slog.Any("request", migrateHttpsDomainReq), slog.Any("response", migrateHttpsDomainResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'console.MigrateHttpsDomain': %w", err) } } return &deployer.DeployResult{}, nil } func createSDKClient(username, password string) (*upyunsdk.Client, error) { return upyunsdk.NewClient(username, password) } ================================================ FILE: pkg/core/deployer/providers/upyun-file/upyun_file_test.go ================================================ package upyunfile_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/upyun-file" ) var ( fInputCertPath string fInputKeyPath string fUsername string fPassword string fBucket string fDomain string ) func init() { argsPrefix := "UPYUNFILE_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "") flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "") flag.StringVar(&fBucket, argsPrefix+"BUCKET", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./upyun_file_test.go -args \ --UPYUNFILE_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --UPYUNFILE_INPUTKEYPATH="/path/to/your-input-key.pem" \ --UPYUNFILE_USERNAME="your-username" \ --UPYUNFILE_PASSWORD="your-password" \ --UPYUNFILE_BUCKET="your-bucket" \ --UPYUNFILE_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("USERNAME: %v", fUsername), fmt.Sprintf("PASSWORD: %v", fPassword), fmt.Sprintf("BUCKET: %v", fBucket), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ Username: fUsername, Password: fPassword, Bucket: fBucket, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/volcengine-alb/consts.go ================================================ package volcenginealb const ( // 资源类型:部署到指定负载均衡器。 RESOURCE_TYPE_LOADBALANCER = "loadbalancer" // 资源类型:部署到指定监听器。 RESOURCE_TYPE_LISTENER = "listener" ) ================================================ FILE: pkg/core/deployer/providers/volcengine-alb/internal/client.go ================================================ package internal import ( "github.com/volcengine/volcengine-go-sdk/service/alb" "github.com/volcengine/volcengine-go-sdk/volcengine" "github.com/volcengine/volcengine-go-sdk/volcengine/client" "github.com/volcengine/volcengine-go-sdk/volcengine/client/metadata" "github.com/volcengine/volcengine-go-sdk/volcengine/corehandlers" "github.com/volcengine/volcengine-go-sdk/volcengine/request" "github.com/volcengine/volcengine-go-sdk/volcengine/signer/volc" "github.com/volcengine/volcengine-go-sdk/volcengine/volcenginequery" ) // This is a partial copy of https://github.com/volcengine/volcengine-go-sdk/blob/master/service/alb/service_alb.go // to lightweight the vendor packages in the built binary. type AlbClient struct { *client.Client } func NewAlbClient(p client.ConfigProvider, cfgs ...*volcengine.Config) *AlbClient { c := p.ClientConfig(alb.EndpointsID, cfgs...) return newAlbClient(*c.Config, c.Handlers, c.Endpoint, c.SigningRegion, c.SigningName) } func newAlbClient(cfg volcengine.Config, handlers request.Handlers, endpoint, signingRegion, signingName string) *AlbClient { svc := &AlbClient{ Client: client.New( cfg, metadata.ClientInfo{ ServiceName: alb.ServiceName, ServiceID: alb.ServiceID, SigningName: signingName, SigningRegion: signingRegion, Endpoint: endpoint, APIVersion: "2020-04-01", }, handlers, ), } svc.Handlers.Build.PushBackNamed(corehandlers.SDKVersionUserAgentHandler) svc.Handlers.Build.PushBackNamed(corehandlers.AddHostExecEnvUserAgentHandler) svc.Handlers.Sign.PushBackNamed(volc.SignRequestHandler) svc.Handlers.Build.PushBackNamed(volcenginequery.BuildHandler) svc.Handlers.Unmarshal.PushBackNamed(volcenginequery.UnmarshalHandler) svc.Handlers.UnmarshalMeta.PushBackNamed(volcenginequery.UnmarshalMetaHandler) svc.Handlers.UnmarshalError.PushBackNamed(volcenginequery.UnmarshalErrorHandler) return svc } func (c *AlbClient) newRequest(op *request.Operation, params, data interface{}) *request.Request { req := c.NewRequest(op, params, data) return req } func (c *AlbClient) DescribeListenerAttributes(input *alb.DescribeListenerAttributesInput) (*alb.DescribeListenerAttributesOutput, error) { req, out := c.DescribeListenerAttributesRequest(input) return out, req.Send() } func (c *AlbClient) DescribeListenerAttributesRequest(input *alb.DescribeListenerAttributesInput) (req *request.Request, output *alb.DescribeListenerAttributesOutput) { op := &request.Operation{ Name: "DescribeListenerAttributes", HTTPMethod: "GET", HTTPPath: "/", } if input == nil { input = &alb.DescribeListenerAttributesInput{} } output = &alb.DescribeListenerAttributesOutput{} req = c.newRequest(op, input, output) return } func (c *AlbClient) DescribeListeners(input *alb.DescribeListenersInput) (*alb.DescribeListenersOutput, error) { req, out := c.DescribeListenersRequest(input) return out, req.Send() } func (c *AlbClient) DescribeListenersRequest(input *alb.DescribeListenersInput) (req *request.Request, output *alb.DescribeListenersOutput) { op := &request.Operation{ Name: "DescribeListeners", HTTPMethod: "GET", HTTPPath: "/", } if input == nil { input = &alb.DescribeListenersInput{} } output = &alb.DescribeListenersOutput{} req = c.newRequest(op, input, output) return } func (c *AlbClient) DescribeLoadBalancerAttributes(input *alb.DescribeLoadBalancerAttributesInput) (*alb.DescribeLoadBalancerAttributesOutput, error) { req, out := c.DescribeLoadBalancerAttributesRequest(input) return out, req.Send() } func (c *AlbClient) DescribeLoadBalancerAttributesRequest(input *alb.DescribeLoadBalancerAttributesInput) (req *request.Request, output *alb.DescribeLoadBalancerAttributesOutput) { op := &request.Operation{ Name: "DescribeLoadBalancerAttributes", HTTPMethod: "GET", HTTPPath: "/", } if input == nil { input = &alb.DescribeLoadBalancerAttributesInput{} } output = &alb.DescribeLoadBalancerAttributesOutput{} req = c.newRequest(op, input, output) return } func (c *AlbClient) ModifyListenerAttributes(input *alb.ModifyListenerAttributesInput) (*alb.ModifyListenerAttributesOutput, error) { req, out := c.ModifyListenerAttributesRequest(input) return out, req.Send() } func (c *AlbClient) ModifyListenerAttributesRequest(input *alb.ModifyListenerAttributesInput) (req *request.Request, output *alb.ModifyListenerAttributesOutput) { op := &request.Operation{ Name: "ModifyListenerAttributes", HTTPMethod: "GET", HTTPPath: "/", } if input == nil { input = &alb.ModifyListenerAttributesInput{} } output = &alb.ModifyListenerAttributesOutput{} req = c.newRequest(op, input, output) return } ================================================ FILE: pkg/core/deployer/providers/volcengine-alb/volcengine_alb.go ================================================ package volcenginealb import ( "context" "errors" "fmt" "log/slog" "github.com/samber/lo" vealb "github.com/volcengine/volcengine-go-sdk/service/alb" ve "github.com/volcengine/volcengine-go-sdk/volcengine" vesession "github.com/volcengine/volcengine-go-sdk/volcengine/session" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-certcenter" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-alb/internal" ) type DeployerConfig struct { // 火山引擎 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 火山引擎 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 火山引擎地域。 Region string `json:"region"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 负载均衡实例 ID。 // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER] 时必填。 LoadbalancerId string `json:"loadbalancerId,omitempty"` // 负载均衡监听器 ID。 // 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。 ListenerId string `json:"listenerId,omitempty"` // SNI 域名(支持泛域名)。 // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时选填。 Domain string `json:"domain,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.AlbClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, Region: config.Region, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_LOADBALANCER: if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil { return nil, err } case RESOURCE_TYPE_LISTENER: if err := d.deployToListener(ctx, upres.CertId); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error { if d.config.LoadbalancerId == "" { return errors.New("config `loadbalancerId` is required") } // 查询 ALB 实例的详细信息 // REF: https://www.volcengine.com/docs/6767/113596 describeLoadBalancerAttributesReq := &vealb.DescribeLoadBalancerAttributesInput{ LoadBalancerId: ve.String(d.config.LoadbalancerId), } describeLoadBalancerAttributesResp, err := d.sdkClient.DescribeLoadBalancerAttributes(describeLoadBalancerAttributesReq) d.logger.Debug("sdk request 'alb.DescribeLoadBalancerAttributes'", slog.Any("request", describeLoadBalancerAttributesReq), slog.Any("response", describeLoadBalancerAttributesResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'alb.DescribeLoadBalancerAttributes': %w", err) } // 查询 HTTPS 监听器列表 // REF: https://www.volcengine.com/docs/6767/113684 listenerIds := make([]string, 0) describeListenersPageSize := 100 describeListenersPageNumber := 1 for { select { case <-ctx.Done(): return ctx.Err() default: } describeListenersReq := &vealb.DescribeListenersInput{ LoadBalancerId: ve.String(d.config.LoadbalancerId), Protocol: ve.String("HTTPS"), PageNumber: ve.Int64(int64(describeListenersPageNumber)), PageSize: ve.Int64(int64(describeListenersPageSize)), } describeListenersResp, err := d.sdkClient.DescribeListeners(describeListenersReq) d.logger.Debug("sdk request 'alb.DescribeListeners'", slog.Any("request", describeListenersReq), slog.Any("response", describeListenersResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'alb.DescribeListeners': %w", err) } for _, listener := range describeListenersResp.Listeners { listenerIds = append(listenerIds, *listener.ListenerId) } if len(describeListenersResp.Listeners) < describeListenersPageSize { break } describeListenersPageNumber++ } // 遍历更新监听证书 if len(listenerIds) == 0 { d.logger.Info("no alb listeners to deploy") } else { d.logger.Info("found https listeners to deploy", slog.Any("listenerIds", listenerIds)) var errs []error for _, listenerId := range listenerIds { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error { if d.config.ListenerId == "" { return errors.New("config `listenerId` is required") } if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil { return err } return nil } func (d *Deployer) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error { // 查询指定监听器的详细信息 // REF: https://www.volcengine.com/docs/6767/113686 describeListenerAttributesReq := &vealb.DescribeListenerAttributesInput{ ListenerId: ve.String(cloudListenerId), } describeListenerAttributesResp, err := d.sdkClient.DescribeListenerAttributes(describeListenerAttributesReq) d.logger.Debug("sdk request 'alb.DescribeListenerAttributes'", slog.Any("request", describeListenerAttributesReq), slog.Any("response", describeListenerAttributesResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'alb.DescribeListenerAttributes': %w", err) } if d.config.Domain == "" { // 未指定 SNI,只需部署到监听器 // 修改指定监听器 // REF: https://www.volcengine.com/docs/6767/113683 modifyListenerAttributesReq := &vealb.ModifyListenerAttributesInput{ ListenerId: ve.String(cloudListenerId), CertificateSource: ve.String("cert_center"), CertCenterCertificateId: ve.String(cloudCertId), } modifyListenerAttributesResp, err := d.sdkClient.ModifyListenerAttributes(modifyListenerAttributesReq) d.logger.Debug("sdk request 'alb.ModifyListenerAttributes'", slog.Any("request", modifyListenerAttributesReq), slog.Any("response", modifyListenerAttributesResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'alb.ModifyListenerAttributes': %w", err) } } else { // 指定 SNI,需部署到扩展域名 // 修改指定监听器 // REF: https://www.volcengine.com/docs/6767/113683 modifyListenerAttributesReq := &vealb.ModifyListenerAttributesInput{ ListenerId: ve.String(cloudListenerId), DomainExtensions: lo.Map( lo.Filter( describeListenerAttributesResp.DomainExtensions, func(domain *vealb.DomainExtensionForDescribeListenerAttributesOutput, _ int) bool { return *domain.Domain == d.config.Domain }, ), func(domain *vealb.DomainExtensionForDescribeListenerAttributesOutput, _ int) *vealb.DomainExtensionForModifyListenerAttributesInput { return &vealb.DomainExtensionForModifyListenerAttributesInput{ DomainExtensionId: domain.DomainExtensionId, Domain: domain.Domain, CertificateSource: ve.String("cert_center"), CertCenterCertificateId: ve.String(cloudCertId), Action: ve.String("modify"), } }), } modifyListenerAttributesResp, err := d.sdkClient.ModifyListenerAttributes(modifyListenerAttributesReq) d.logger.Debug("sdk request 'alb.ModifyListenerAttributes'", slog.Any("request", modifyListenerAttributesReq), slog.Any("response", modifyListenerAttributesResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'alb.ModifyListenerAttributes': %w", err) } } return nil } func createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.AlbClient, error) { config := ve.NewConfig(). WithAkSk(accessKeyId, accessKeySecret). WithRegion(region) session, err := vesession.NewSession(config) if err != nil { return nil, err } client := internal.NewAlbClient(session) return client, nil } ================================================ FILE: pkg/core/deployer/providers/volcengine-alb/volcengine_alb_test.go ================================================ package volcenginealb_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-alb" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fRegion string fListenerId string ) func init() { argsPrefix := "VOLCENGINEALB_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "") } /* Shell command to run this test: go test -v ./volcengine_alb_test.go -args \ --VOLCENGINEALB_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --VOLCENGINEALB_INPUTKEYPATH="/path/to/your-input-key.pem" \ --VOLCENGINEALB_ACCESSKEYID="your-access-key-id" \ --VOLCENGINEALB_ACCESSKEYSECRET="your-access-key-secret" \ --VOLCENGINEALB_REGION="cn-beijing" \ --VOLCENGINEALB_LISTENERID="your-listener-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("LISTENERID: %v", fListenerId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Region: fRegion, ResourceType: provider.RESOURCE_TYPE_LISTENER, ListenerId: fListenerId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/volcengine-cdn/consts.go ================================================ package volcenginecdn const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/volcengine-cdn/internal/client.go ================================================ package internal import ( "github.com/volcengine/volcengine-go-sdk/service/cdn" "github.com/volcengine/volcengine-go-sdk/volcengine" "github.com/volcengine/volcengine-go-sdk/volcengine/client" "github.com/volcengine/volcengine-go-sdk/volcengine/client/metadata" "github.com/volcengine/volcengine-go-sdk/volcengine/corehandlers" "github.com/volcengine/volcengine-go-sdk/volcengine/request" "github.com/volcengine/volcengine-go-sdk/volcengine/signer/volc" "github.com/volcengine/volcengine-go-sdk/volcengine/volcenginequery" ) // This is a partial copy of https://github.com/volcengine/volcengine-go-sdk/blob/master/service/cdn/service_cdn.go // to lightweight the vendor packages in the built binary. type CdnClient struct { *client.Client } func NewCdnClient(p client.ConfigProvider, cfgs ...*volcengine.Config) *CdnClient { c := p.ClientConfig(cdn.EndpointsID, cfgs...) return newCdnClient(*c.Config, c.Handlers, c.Endpoint, c.SigningRegion, c.SigningName) } func newCdnClient(cfg volcengine.Config, handlers request.Handlers, endpoint, signingRegion, signingName string) *CdnClient { svc := &CdnClient{ Client: client.New( cfg, metadata.ClientInfo{ ServiceName: cdn.ServiceName, ServiceID: cdn.ServiceID, SigningName: signingName, SigningRegion: signingRegion, Endpoint: endpoint, APIVersion: "2021-03-01", }, handlers, ), } svc.Handlers.Build.PushBackNamed(corehandlers.SDKVersionUserAgentHandler) svc.Handlers.Build.PushBackNamed(corehandlers.AddHostExecEnvUserAgentHandler) svc.Handlers.Sign.PushBackNamed(volc.SignRequestHandler) svc.Handlers.Build.PushBackNamed(volcenginequery.BuildHandler) svc.Handlers.Unmarshal.PushBackNamed(volcenginequery.UnmarshalHandler) svc.Handlers.UnmarshalMeta.PushBackNamed(volcenginequery.UnmarshalMetaHandler) svc.Handlers.UnmarshalError.PushBackNamed(volcenginequery.UnmarshalErrorHandler) return svc } func (c *CdnClient) newRequest(op *request.Operation, params, data interface{}) *request.Request { req := c.NewRequest(op, params, data) return req } func (c *CdnClient) BatchDeployCert(input *cdn.BatchDeployCertInput) (*cdn.BatchDeployCertOutput, error) { req, out := c.BatchDeployCertRequest(input) return out, req.Send() } func (c *CdnClient) BatchDeployCertRequest(input *cdn.BatchDeployCertInput) (req *request.Request, output *cdn.BatchDeployCertOutput) { op := &request.Operation{ Name: "BatchDeployCert", HTTPMethod: "POST", HTTPPath: "/", } if input == nil { input = &cdn.BatchDeployCertInput{} } output = &cdn.BatchDeployCertOutput{} req = c.newRequest(op, input, output) req.HTTPRequest.Header.Set("Content-Type", "application/json; charset=utf-8") return } func (c *CdnClient) DescribeCertConfig(input *cdn.DescribeCertConfigInput) (*cdn.DescribeCertConfigOutput, error) { req, out := c.DescribeCertConfigRequest(input) return out, req.Send() } func (c *CdnClient) DescribeCertConfigRequest(input *cdn.DescribeCertConfigInput) (req *request.Request, output *cdn.DescribeCertConfigOutput) { op := &request.Operation{ Name: "DescribeCertConfig", HTTPMethod: "POST", HTTPPath: "/", } if input == nil { input = &cdn.DescribeCertConfigInput{} } output = &cdn.DescribeCertConfigOutput{} req = c.newRequest(op, input, output) req.HTTPRequest.Header.Set("Content-Type", "application/json; charset=utf-8") return } func (c *CdnClient) ListCdnDomains(input *cdn.ListCdnDomainsInput) (*cdn.ListCdnDomainsOutput, error) { req, out := c.ListCdnDomainsRequest(input) return out, req.Send() } func (c *CdnClient) ListCdnDomainsRequest(input *cdn.ListCdnDomainsInput) (req *request.Request, output *cdn.ListCdnDomainsOutput) { op := &request.Operation{ Name: "ListCdnDomains", HTTPMethod: "POST", HTTPPath: "/", } if input == nil { input = &cdn.ListCdnDomainsInput{} } output = &cdn.ListCdnDomainsOutput{} req = c.newRequest(op, input, output) req.HTTPRequest.Header.Set("Content-Type", "application/json; charset=utf-8") return } ================================================ FILE: pkg/core/deployer/providers/volcengine-cdn/volcengine_cdn.go ================================================ package volcenginecdn import ( "context" "errors" "fmt" "log/slog" "strings" vecdn "github.com/volcengine/volcengine-go-sdk/service/cdn" ve "github.com/volcengine/volcengine-go-sdk/volcengine" vesession "github.com/volcengine/volcengine-go-sdk/volcengine/session" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-cdn" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-cdn/internal" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 火山引擎 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 火山引擎 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.CdnClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名列表 domains := make([]string, 0) switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = []string{d.config.Domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getMatchedDomainsByWildcard(ctx, d.config.Domain) if err != nil { return nil, err } domains = domainCandidates } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { domainCandidates, err := d.getMatchedDomainsByCertId(ctx, upres.CertId) if err != nil { return nil, err } domains = domainCandidates } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历绑定证书 if len(domains) == 0 { d.logger.Info("no cdn domains to deploy") } else { d.logger.Info("found cdn domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getMatchedDomainsByWildcard(ctx context.Context, wildcardDomain string) ([]string, error) { domains := make([]string, 0) // 查询加速域名列表,获取匹配的域名 // REF: https://www.volcengine.com/docs/6454/75269 listCdnDomainsPageNum := 1 listCdnDomainsPageSize := 100 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } listCdnDomainsReq := &vecdn.ListCdnDomainsInput{ Domain: ve.String(strings.TrimPrefix(wildcardDomain, "*.")), Status: ve.String("online"), PageNum: ve.Int64(int64(listCdnDomainsPageNum)), PageSize: ve.Int64(int64(listCdnDomainsPageSize)), } listCdnDomainsResp, err := d.sdkClient.ListCdnDomains(listCdnDomainsReq) d.logger.Debug("sdk request 'cdn.ListCdnDomains'", slog.Any("request", listCdnDomainsReq), slog.Any("response", listCdnDomainsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.ListCdnDomains': %w", err) } for _, domainItem := range listCdnDomainsResp.Data { if xcerthostname.IsMatch(wildcardDomain, ve.StringValue(domainItem.Domain)) { domains = append(domains, ve.StringValue(domainItem.Domain)) } } if len(listCdnDomainsResp.Data) < listCdnDomainsPageSize { break } listCdnDomainsPageSize++ } if len(domains) == 0 { return nil, errors.New("could not find any domains matched by wildcard") } return domains, nil } func (d *Deployer) getMatchedDomainsByCertId(ctx context.Context, cloudCertId string) ([]string, error) { domains := make([]string, 0) // 获取指定证书可关联的域名 // REF: https://www.volcengine.com/docs/6454/125711 describeCertConfigReq := &vecdn.DescribeCertConfigInput{ CertId: ve.String(cloudCertId), } describeCertConfigResp, err := d.sdkClient.DescribeCertConfig(describeCertConfigReq) d.logger.Debug("sdk request 'cdn.DescribeCertConfig'", slog.Any("request", describeCertConfigReq), slog.Any("response", describeCertConfigResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.DescribeCertConfig': %w", err) } if describeCertConfigResp.CertNotConfig != nil { for i := range describeCertConfigResp.CertNotConfig { domains = append(domains, ve.StringValue(describeCertConfigResp.CertNotConfig[i].Domain)) } } if describeCertConfigResp.OtherCertConfig != nil { for i := range describeCertConfigResp.OtherCertConfig { domains = append(domains, ve.StringValue(describeCertConfigResp.OtherCertConfig[i].Domain)) } } if len(domains) == 0 { if len(describeCertConfigResp.SpecifiedCertConfig) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error { // 关联证书与加速域名 // REF: https://www.volcengine.com/docs/6454/125712 batchDeployCertReq := &vecdn.BatchDeployCertInput{ Domain: ve.String(domain), CertId: ve.String(cloudCertId), } batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq) d.logger.Debug("sdk request 'cdn.BatchDeployCert'", slog.Any("request", batchDeployCertReq), slog.Any("response", batchDeployCertResp)) if err != nil { return err } return nil } func createSDKClient(accessKeyId, accessKeySecret string) (*internal.CdnClient, error) { config := ve.NewConfig(). WithAkSk(accessKeyId, accessKeySecret). WithRegion("cn-north-1") session, err := vesession.NewSession(config) if err != nil { return nil, err } client := internal.NewCdnClient(session) return client, nil } ================================================ FILE: pkg/core/deployer/providers/volcengine-cdn/volcengine_cdn_test.go ================================================ package volcenginecdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-cdn" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fDomain string ) func init() { argsPrefix := "VOLCENGINECDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./volcengine_cdn_test.go -args \ --VOLCENGINECDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --VOLCENGINECDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --VOLCENGINECDN_ACCESSKEYID="your-access-key-id" \ --VOLCENGINECDN_ACCESSKEYSECRET="your-access-key-secret" \ --VOLCENGINECDN_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/volcengine-certcenter/volcengine_certcenter.go ================================================ package volcenginecertcenter import ( "context" "errors" "fmt" "log/slog" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-certcenter" "github.com/certimate-go/certimate/pkg/core/deployer" ) type DeployerConfig struct { // 火山引擎 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 火山引擎 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 火山引擎地域。 Region string `json:"region"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, Region: config.Region, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } return &deployer.DeployResult{}, nil } ================================================ FILE: pkg/core/deployer/providers/volcengine-clb/consts.go ================================================ package volcengineclb const ( // 资源类型:部署到指定负载均衡器。 RESOURCE_TYPE_LOADBALANCER = "loadbalancer" // 资源类型:部署到指定监听器。 RESOURCE_TYPE_LISTENER = "listener" ) ================================================ FILE: pkg/core/deployer/providers/volcengine-clb/internal/client.go ================================================ package internal import ( "github.com/volcengine/volcengine-go-sdk/service/clb" "github.com/volcengine/volcengine-go-sdk/volcengine" "github.com/volcengine/volcengine-go-sdk/volcengine/client" "github.com/volcengine/volcengine-go-sdk/volcengine/client/metadata" "github.com/volcengine/volcengine-go-sdk/volcengine/corehandlers" "github.com/volcengine/volcengine-go-sdk/volcengine/request" "github.com/volcengine/volcengine-go-sdk/volcengine/signer/volc" "github.com/volcengine/volcengine-go-sdk/volcengine/volcenginequery" ) // This is a partial copy of https://github.com/volcengine/volcengine-go-sdk/blob/master/service/clb/service_clb.go // to lightweight the vendor packages in the built binary. type ClbClient struct { *client.Client } func NewClbClient(p client.ConfigProvider, cfgs ...*volcengine.Config) *ClbClient { c := p.ClientConfig(clb.EndpointsID, cfgs...) return newClbClient(*c.Config, c.Handlers, c.Endpoint, c.SigningRegion, c.SigningName) } func newClbClient(cfg volcengine.Config, handlers request.Handlers, endpoint, signingRegion, signingName string) *ClbClient { svc := &ClbClient{ Client: client.New( cfg, metadata.ClientInfo{ ServiceName: clb.ServiceName, ServiceID: clb.ServiceID, SigningName: signingName, SigningRegion: signingRegion, Endpoint: endpoint, APIVersion: "2020-04-01", }, handlers, ), } svc.Handlers.Build.PushBackNamed(corehandlers.SDKVersionUserAgentHandler) svc.Handlers.Build.PushBackNamed(corehandlers.AddHostExecEnvUserAgentHandler) svc.Handlers.Sign.PushBackNamed(volc.SignRequestHandler) svc.Handlers.Build.PushBackNamed(volcenginequery.BuildHandler) svc.Handlers.Unmarshal.PushBackNamed(volcenginequery.UnmarshalHandler) svc.Handlers.UnmarshalMeta.PushBackNamed(volcenginequery.UnmarshalMetaHandler) svc.Handlers.UnmarshalError.PushBackNamed(volcenginequery.UnmarshalErrorHandler) return svc } func (c *ClbClient) newRequest(op *request.Operation, params, data interface{}) *request.Request { req := c.NewRequest(op, params, data) return req } func (c *ClbClient) DescribeListeners(input *clb.DescribeListenersInput) (*clb.DescribeListenersOutput, error) { req, out := c.DescribeListenersRequest(input) return out, req.Send() } func (c *ClbClient) DescribeListenersRequest(input *clb.DescribeListenersInput) (req *request.Request, output *clb.DescribeListenersOutput) { op := &request.Operation{ Name: "DescribeListeners", HTTPMethod: "GET", HTTPPath: "/", } if input == nil { input = &clb.DescribeListenersInput{} } output = &clb.DescribeListenersOutput{} req = c.newRequest(op, input, output) return } func (c *ClbClient) DescribeLoadBalancerAttributes(input *clb.DescribeLoadBalancerAttributesInput) (*clb.DescribeLoadBalancerAttributesOutput, error) { req, out := c.DescribeLoadBalancerAttributesRequest(input) return out, req.Send() } func (c *ClbClient) DescribeLoadBalancerAttributesRequest(input *clb.DescribeLoadBalancerAttributesInput) (req *request.Request, output *clb.DescribeLoadBalancerAttributesOutput) { op := &request.Operation{ Name: "DescribeLoadBalancerAttributes", HTTPMethod: "GET", HTTPPath: "/", } if input == nil { input = &clb.DescribeLoadBalancerAttributesInput{} } output = &clb.DescribeLoadBalancerAttributesOutput{} req = c.newRequest(op, input, output) return } func (c *ClbClient) ModifyListenerAttributes(input *clb.ModifyListenerAttributesInput) (*clb.ModifyListenerAttributesOutput, error) { req, out := c.ModifyListenerAttributesRequest(input) return out, req.Send() } func (c *ClbClient) ModifyListenerAttributesRequest(input *clb.ModifyListenerAttributesInput) (req *request.Request, output *clb.ModifyListenerAttributesOutput) { op := &request.Operation{ Name: "ModifyListenerAttributes", HTTPMethod: "GET", HTTPPath: "/", } if input == nil { input = &clb.ModifyListenerAttributesInput{} } output = &clb.ModifyListenerAttributesOutput{} req = c.newRequest(op, input, output) return } ================================================ FILE: pkg/core/deployer/providers/volcengine-clb/volcengine_clb.go ================================================ package volcengineclb import ( "context" "errors" "fmt" "log/slog" veclb "github.com/volcengine/volcengine-go-sdk/service/clb" ve "github.com/volcengine/volcengine-go-sdk/volcengine" vesession "github.com/volcengine/volcengine-go-sdk/volcengine/session" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-certcenter" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-clb/internal" ) type DeployerConfig struct { // 火山引擎 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 火山引擎 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 火山引擎地域。 Region string `json:"region"` // 部署资源类型。 ResourceType string `json:"resourceType"` // 负载均衡实例 ID。 // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER] 时必填。 LoadbalancerId string `json:"loadbalancerId,omitempty"` // 负载均衡监听器 ID。 // 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。 ListenerId string `json:"listenerId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.ClbClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, Region: config.Region, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_LOADBALANCER: if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil { return nil, err } case RESOURCE_TYPE_LISTENER: if err := d.deployToListener(ctx, upres.CertId); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error { if d.config.LoadbalancerId == "" { return errors.New("config `loadbalancerId` is required") } // 查看指定负载均衡实例的详情 // REF: https://www.volcengine.com/docs/6406/71773 describeLoadBalancerAttributesReq := &veclb.DescribeLoadBalancerAttributesInput{ LoadBalancerId: ve.String(d.config.LoadbalancerId), } describeLoadBalancerAttributesResp, err := d.sdkClient.DescribeLoadBalancerAttributes(describeLoadBalancerAttributesReq) d.logger.Debug("sdk request 'clb.DescribeLoadBalancerAttributes'", slog.Any("request", describeLoadBalancerAttributesReq), slog.Any("response", describeLoadBalancerAttributesResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'clb.DescribeLoadBalancerAttributes': %w", err) } // 查询 HTTPS 监听器列表 // REF: https://www.volcengine.com/docs/6406/71776 listenerIds := make([]string, 0) describeListenersPageSize := 100 describeListenersPageNumber := 1 for { select { case <-ctx.Done(): return ctx.Err() default: } describeListenersReq := &veclb.DescribeListenersInput{ LoadBalancerId: ve.String(d.config.LoadbalancerId), Protocol: ve.String("HTTPS"), PageNumber: ve.Int64(int64(describeListenersPageNumber)), PageSize: ve.Int64(int64(describeListenersPageSize)), } describeListenersResp, err := d.sdkClient.DescribeListeners(describeListenersReq) d.logger.Debug("sdk request 'clb.DescribeListeners'", slog.Any("request", describeListenersReq), slog.Any("response", describeListenersResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'clb.DescribeListeners': %w", err) } for _, listener := range describeListenersResp.Listeners { listenerIds = append(listenerIds, *listener.ListenerId) } if len(describeListenersResp.Listeners) < describeListenersPageSize { break } describeListenersPageNumber++ } // 遍历更新监听证书 if len(listenerIds) == 0 { d.logger.Info("no clb listeners to deploy") } else { d.logger.Info("found https listeners to deploy", slog.Any("listenerIds", listenerIds)) var errs []error for _, listenerId := range listenerIds { select { case <-ctx.Done(): return ctx.Err() default: if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return errors.Join(errs...) } } return nil } func (d *Deployer) deployToListener(ctx context.Context, cloudCertId string) error { if d.config.ListenerId == "" { return errors.New("config `listenerId` is required") } if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil { return err } return nil } func (d *Deployer) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error { // 修改指定监听器 // REF: https://www.volcengine.com/docs/6406/71775 modifyListenerAttributesReq := &veclb.ModifyListenerAttributesInput{ ListenerId: ve.String(cloudListenerId), CertificateSource: ve.String("cert_center"), CertCenterCertificateId: ve.String(cloudCertId), } modifyListenerAttributesResp, err := d.sdkClient.ModifyListenerAttributes(modifyListenerAttributesReq) d.logger.Debug("sdk request 'clb.ModifyListenerAttributes'", slog.Any("request", modifyListenerAttributesReq), slog.Any("response", modifyListenerAttributesResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'clb.ModifyListenerAttributes': %w", err) } return nil } func createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.ClbClient, error) { config := ve.NewConfig(). WithAkSk(accessKeyId, accessKeySecret). WithRegion(region) session, err := vesession.NewSession(config) if err != nil { return nil, err } client := internal.NewClbClient(session) return client, nil } ================================================ FILE: pkg/core/deployer/providers/volcengine-clb/volcengine_clb_test.go ================================================ package volcengineclb_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-clb" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fRegion string fListenerId string ) func init() { argsPrefix := "VOLCENGINECLB_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "") } /* Shell command to run this test: go test -v ./volcengine_clb_test.go -args \ --VOLCENGINECLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --VOLCENGINECLB_INPUTKEYPATH="/path/to/your-input-key.pem" \ --VOLCENGINECLB_ACCESSKEYID="your-access-key-id" \ --VOLCENGINECLB_ACCESSKEYSECRET="your-access-key-secret" \ --VOLCENGINECLB_REGION="cn-beijing" \ --VOLCENGINECLB_LISTENERID="your-listener-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("LISTENERID: %v", fListenerId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Region: fRegion, ResourceType: provider.RESOURCE_TYPE_LISTENER, ListenerId: fListenerId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/volcengine-dcdn/consts.go ================================================ package volcenginedcdn const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/volcengine-dcdn/internal/client.go ================================================ package internal import ( "github.com/volcengine/volcengine-go-sdk/service/dcdn" "github.com/volcengine/volcengine-go-sdk/volcengine" "github.com/volcengine/volcengine-go-sdk/volcengine/client" "github.com/volcengine/volcengine-go-sdk/volcengine/client/metadata" "github.com/volcengine/volcengine-go-sdk/volcengine/corehandlers" "github.com/volcengine/volcengine-go-sdk/volcengine/request" "github.com/volcengine/volcengine-go-sdk/volcengine/signer/volc" "github.com/volcengine/volcengine-go-sdk/volcengine/volcenginequery" ) // This is a partial copy of https://github.com/volcengine/volcengine-go-sdk/blob/master/service/dcdn/service_dcdn.go // to lightweight the vendor packages in the built binary. type DcdnClient struct { *client.Client } func NewDcdnClient(p client.ConfigProvider, cfgs ...*volcengine.Config) *DcdnClient { c := p.ClientConfig(dcdn.EndpointsID, cfgs...) return newDcdnClient(*c.Config, c.Handlers, c.Endpoint, c.SigningRegion, c.SigningName) } func newDcdnClient(cfg volcengine.Config, handlers request.Handlers, endpoint, signingRegion, signingName string) *DcdnClient { svc := &DcdnClient{ Client: client.New( cfg, metadata.ClientInfo{ ServiceName: dcdn.ServiceName, ServiceID: dcdn.ServiceID, SigningName: signingName, SigningRegion: signingRegion, Endpoint: endpoint, APIVersion: "2021-04-01", }, handlers, ), } svc.Handlers.Build.PushBackNamed(corehandlers.SDKVersionUserAgentHandler) svc.Handlers.Build.PushBackNamed(corehandlers.AddHostExecEnvUserAgentHandler) svc.Handlers.Sign.PushBackNamed(volc.SignRequestHandler) svc.Handlers.Build.PushBackNamed(volcenginequery.BuildHandler) svc.Handlers.Unmarshal.PushBackNamed(volcenginequery.UnmarshalHandler) svc.Handlers.UnmarshalMeta.PushBackNamed(volcenginequery.UnmarshalMetaHandler) svc.Handlers.UnmarshalError.PushBackNamed(volcenginequery.UnmarshalErrorHandler) return svc } func (c *DcdnClient) newRequest(op *request.Operation, params, data interface{}) *request.Request { req := c.NewRequest(op, params, data) return req } func (c *DcdnClient) CreateCertBind(input *dcdn.CreateCertBindInput) (*dcdn.CreateCertBindOutput, error) { req, out := c.CreateCertBindRequest(input) return out, req.Send() } func (c *DcdnClient) CreateCertBindRequest(input *dcdn.CreateCertBindInput) (req *request.Request, output *dcdn.CreateCertBindOutput) { op := &request.Operation{ Name: "CreateCertBind", HTTPMethod: "POST", HTTPPath: "/", } if input == nil { input = &dcdn.CreateCertBindInput{} } output = &dcdn.CreateCertBindOutput{} req = c.newRequest(op, input, output) req.HTTPRequest.Header.Set("Content-Type", "application/json; charset=utf-8") return } func (c *DcdnClient) ListDomainConfig(input *dcdn.ListDomainConfigInput) (*dcdn.ListDomainConfigOutput, error) { req, out := c.ListDomainConfigRequest(input) return out, req.Send() } func (c *DcdnClient) ListDomainConfigRequest(input *dcdn.ListDomainConfigInput) (req *request.Request, output *dcdn.ListDomainConfigOutput) { op := &request.Operation{ Name: "ListDomainConfig", HTTPMethod: "POST", HTTPPath: "/", } if input == nil { input = &dcdn.ListDomainConfigInput{} } output = &dcdn.ListDomainConfigOutput{} req = c.newRequest(op, input, output) req.HTTPRequest.Header.Set("Content-Type", "application/json; charset=utf-8") return } ================================================ FILE: pkg/core/deployer/providers/volcengine-dcdn/volcengine_dcdn.go ================================================ package volcenginedcdn import ( "context" "errors" "fmt" "log/slog" "strings" "github.com/samber/lo" vedcdn "github.com/volcengine/volcengine-go-sdk/service/dcdn" ve "github.com/volcengine/volcengine-go-sdk/volcengine" vesession "github.com/volcengine/volcengine-go-sdk/volcengine/session" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-certcenter" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-dcdn/internal" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 火山引擎 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 火山引擎 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 火山引擎地域。 Region string `json:"region"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.DcdnClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, Region: config.Region, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名列表 domains := make([]string, 0) switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } // "*.example.com" → ".example.com",适配火山引擎 DCDN 要求的泛域名格式 domain := strings.TrimPrefix(d.config.Domain, "*") domains = []string{domain} } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return xcerthostname.IsMatch(d.config.Domain, domain) || strings.TrimPrefix(d.config.Domain, "*") == strings.TrimPrefix(domain, "*") }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by wildcard") } } else { domains = []string{d.config.Domain} } } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil || strings.TrimPrefix(d.config.Domain, "*") == strings.TrimPrefix(domain, "*") }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 批量绑定证书 // REF: https://www.volcengine.com/docs/6559/1250189 createCertBindReq := &vedcdn.CreateCertBindInput{ CertSource: ve.String("volc"), CertId: ve.String(upres.CertId), DomainNames: ve.StringSlice(domains), } createCertBindResp, err := d.sdkClient.CreateCertBind(createCertBindReq) d.logger.Debug("sdk request 'dcdn.CreateCertBind'", slog.Any("request", createCertBindReq), slog.Any("response", createCertBindResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'dcdn.CreateCertBind': %w", err) } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 查询域名配置列表 // https://www.volcengine.com/docs/6559/1171745 listDomainConfigPageNumber := 1 listDomainConfigPageSize := 100 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } listDomainConfigReq := &vedcdn.ListDomainConfigInput{ PageNumber: ve.Int32(int32(listDomainConfigPageNumber)), PageSize: ve.Int32(int32(listDomainConfigPageSize)), } listDomainConfigResp, err := d.sdkClient.ListDomainConfig(listDomainConfigReq) d.logger.Debug("sdk request 'dcdn.ListDomainConfig'", slog.Any("request", listDomainConfigReq), slog.Any("response", listDomainConfigResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'dcdn.ListDomainConfig': %w", err) } ignoredStatuses := []string{"Stop"} for _, domainItem := range listDomainConfigResp.DomainList { if lo.Contains(ignoredStatuses, *domainItem.Status) { continue } domains = append(domains, *domainItem.Domain) } if len(listDomainConfigResp.DomainList) < listDomainConfigPageSize { break } listDomainConfigPageNumber++ } return domains, nil } func createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.DcdnClient, error) { if region == "" { region = "cn-beijing" // DCDN 服务默认区域:北京 } config := ve.NewConfig(). WithAkSk(accessKeyId, accessKeySecret). WithRegion(region) session, err := vesession.NewSession(config) if err != nil { return nil, err } client := internal.NewDcdnClient(session) return client, nil } ================================================ FILE: pkg/core/deployer/providers/volcengine-dcdn/volcengine_dcdn_test.go ================================================ package volcenginedcdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-dcdn" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fDomain string ) func init() { argsPrefix := "VOLCENGINEDCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./volcengine_dcdn_test.go -args \ --VOLCENGINEDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --VOLCENGINEDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --VOLCENGINEDCDN_ACCESSKEYID="your-access-key-id" \ --VOLCENGINEDCDN_ACCESSKEYSECRET="your-access-key-secret" \ --VOLCENGINEDCDN_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/volcengine-imagex/volcengine_imagex.go ================================================ package volcengineimagex import ( "context" "errors" "fmt" "log/slog" vebase "github.com/volcengine/volc-sdk-golang/base" veimagex "github.com/volcengine/volc-sdk-golang/service/imagex/v2" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-certcenter" "github.com/certimate-go/certimate/pkg/core/deployer" ) type DeployerConfig struct { // 火山引擎 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 火山引擎 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 火山引擎地域。 Region string `json:"region"` // 服务 ID。 ServiceId string `json:"serviceId"` // 自定义域名(不支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *veimagex.Imagex sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, Region: config.Region, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { if d.config.ServiceId == "" { return nil, errors.New("config `serviceId` is required") } if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取域名配置 // REF: https://www.volcengine.com/docs/508/9366 getDomainConfigReq := &veimagex.GetDomainConfigQuery{ ServiceID: d.config.ServiceId, DomainName: d.config.Domain, } getDomainConfigResp, err := d.sdkClient.GetDomainConfig(ctx, getDomainConfigReq) d.logger.Debug("sdk request 'imagex.GetDomainConfig'", slog.Any("request", getDomainConfigReq), slog.Any("response", getDomainConfigResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'imagex.GetDomainConfig': %w", err) } // 更新 HTTPS 配置 // REF: https://www.volcengine.com/docs/508/66012 updateHttpsReq := &veimagex.UpdateHTTPSReq{ UpdateHTTPSQuery: &veimagex.UpdateHTTPSQuery{ ServiceID: d.config.ServiceId, }, UpdateHTTPSBody: &veimagex.UpdateHTTPSBody{ Domain: d.config.Domain, HTTPS: &veimagex.UpdateHTTPSBodyHTTPS{ CertID: upres.CertId, EnableHTTPS: true, }, }, } if getDomainConfigResp.Result != nil && getDomainConfigResp.Result.HTTPSConfig != nil { updateHttpsReq.UpdateHTTPSBody.HTTPS.EnableHTTPS = getDomainConfigResp.Result.HTTPSConfig.EnableHTTPS updateHttpsReq.UpdateHTTPSBody.HTTPS.EnableHTTP2 = getDomainConfigResp.Result.HTTPSConfig.EnableHTTP2 updateHttpsReq.UpdateHTTPSBody.HTTPS.EnableOcsp = getDomainConfigResp.Result.HTTPSConfig.EnableOcsp updateHttpsReq.UpdateHTTPSBody.HTTPS.TLSVersions = getDomainConfigResp.Result.HTTPSConfig.TLSVersions updateHttpsReq.UpdateHTTPSBody.HTTPS.EnableForceRedirect = getDomainConfigResp.Result.HTTPSConfig.EnableForceRedirect updateHttpsReq.UpdateHTTPSBody.HTTPS.ForceRedirectType = getDomainConfigResp.Result.HTTPSConfig.ForceRedirectType updateHttpsReq.UpdateHTTPSBody.HTTPS.ForceRedirectCode = getDomainConfigResp.Result.HTTPSConfig.ForceRedirectCode } updateHttpsResp, err := d.sdkClient.UpdateHTTPS(ctx, updateHttpsReq) d.logger.Debug("sdk request 'imagex.UpdateHttps'", slog.Any("request", updateHttpsReq), slog.Any("response", updateHttpsResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'imagex.UpdateHttps': %w", err) } return &deployer.DeployResult{}, nil } func createSDKClient(accessKeyId, accessKeySecret, region string) (*veimagex.Imagex, error) { var instance *veimagex.Imagex if region == "" { instance = veimagex.NewInstance() } else { instance = veimagex.NewInstanceWithRegion(region) } instance.SetCredential(vebase.Credentials{ AccessKeyID: accessKeyId, SecretAccessKey: accessKeySecret, }) return instance, nil } ================================================ FILE: pkg/core/deployer/providers/volcengine-imagex/volcengine_imagex_test.go ================================================ package volcengineimagex_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-imagex" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fRegion string fServiceId string fDomain string ) func init() { argsPrefix := "VOLCENGINEIMAGEX_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fServiceId, argsPrefix+"SERVICEID", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./volcengine_imagex_test.go -args \ --VOLCENGINEIMAGEX_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --VOLCENGINEIMAGEX_INPUTKEYPATH="/path/to/your-input-key.pem" \ --VOLCENGINEIMAGEX_ACCESSKEYID="your-access-key-id" \ --VOLCENGINEIMAGEX_ACCESSKEYSECRET="your-access-key-secret" \ --VOLCENGINEIMAGEX_REGION="cn-north-1" \ --VOLCENGINEIMAGEX_SERVICEID="your-service-id" \ --VOLCENGINEIMAGEX_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("SERVICEID: %v", fServiceId), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Region: fRegion, ServiceId: fServiceId, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/volcengine-live/consts.go ================================================ package volcenginelive const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) ================================================ FILE: pkg/core/deployer/providers/volcengine-live/volcengine_live.go ================================================ package volcenginelive import ( "context" "errors" "fmt" "log/slog" "strings" "github.com/samber/lo" velive "github.com/volcengine/volc-sdk-golang/service/live/v20230101" ve "github.com/volcengine/volcengine-go-sdk/volcengine" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-live" "github.com/certimate-go/certimate/pkg/core/deployer" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 火山引擎 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 火山引擎 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 直播流域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *velive.Live sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client := velive.NewInstance() client.SetAccessKey(config.AccessKeyId) client.SetSecretKey(config.AccessKeySecret) pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的直播实例 domains := make([]string, 0) switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = append(domains, d.config.Domain) } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return xcerthostname.IsMatch(d.config.Domain, domain) }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by wildcard") } } else { domains = append(domains, d.config.Domain) } } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历绑定证书 if len(domains) == 0 { d.logger.Info("no live domains to deploy") } else { d.logger.Info("found live domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 查询域名列表 // REF: https://www.volcengine.com/docs/6469/1126815 listDomainDetailPageNum := 1 listDomainDetailPageSize := 1000 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } listDomainDetailReq := &velive.ListDomainDetailBody{ DomainStatusList: ve.Int32Slice([]int32{0}), PageNum: int32(listDomainDetailPageNum), PageSize: int32(listDomainDetailPageSize), } listDomainDetailResp, err := d.sdkClient.ListDomainDetail(ctx, listDomainDetailReq) d.logger.Debug("sdk request 'live.ListDomainDetail'", slog.Any("request", listDomainDetailReq), slog.Any("response", listDomainDetailResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'live.ListDomainDetail': %w", err) } if listDomainDetailResp.Result == nil { break } for _, domainItem := range listDomainDetailResp.Result.DomainList { domains = append(domains, domainItem.Domain) } if len(listDomainDetailResp.Result.DomainList) < listDomainDetailPageSize { break } listDomainDetailPageNum++ } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error { // 绑定证书 // REF: https://www.volcengine.com/docs/6469/1126820 bindCertReq := &velive.BindCertBody{ ChainID: cloudCertId, Domain: domain, HTTPS: ve.Bool(true), } bindCertResp, err := d.sdkClient.BindCert(ctx, bindCertReq) d.logger.Debug("sdk request 'live.BindCert'", slog.Any("request", bindCertReq), slog.Any("response", bindCertResp)) if err != nil { return err } return nil } ================================================ FILE: pkg/core/deployer/providers/volcengine-live/volcengine_live_test.go ================================================ package volcenginelive_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-live" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fDomain string ) func init() { argsPrefix := "VOLCENGINELIVE_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./volcengine_live_test.go -args \ --VOLCENGINELIVE_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --VOLCENGINELIVE_INPUTKEYPATH="/path/to/your-input-key.pem" \ --VOLCENGINELIVE_ACCESSKEYID="your-access-key-id" \ --VOLCENGINELIVE_ACCESSKEYSECRET="your-access-key-secret" \ --VOLCENGINELIVE_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/volcengine-tos/volcengine_tos.go ================================================ package volcenginetos import ( "context" "errors" "fmt" "log/slog" "github.com/volcengine/ve-tos-golang-sdk/v2/tos" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-certcenter" "github.com/certimate-go/certimate/pkg/core/deployer" ) type DeployerConfig struct { // 火山引擎 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 火山引擎 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 火山引擎地域。 Region string `json:"region"` // 存储桶名。 Bucket string `json:"bucket"` // 自定义域名(不支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *tos.ClientV2 sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, Region: config.Region, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { if d.config.Bucket == "" { return nil, errors.New("config `bucket` is required") } if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 设置自定义域名 // REF: https://www.volcengine.com/docs/6349/764779 putBucketCustomDomainReq := &tos.PutBucketCustomDomainInput{ Bucket: d.config.Bucket, Rule: tos.CustomDomainRule{ Domain: d.config.Domain, CertID: upres.CertId, }, } putBucketCustomDomainResp, err := d.sdkClient.PutBucketCustomDomain(ctx, putBucketCustomDomainReq) d.logger.Debug("sdk request 'tos.PutBucketCustomDomain'", slog.Any("request", putBucketCustomDomainReq), slog.Any("response", putBucketCustomDomainResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'tos.PutBucketCustomDomain': %w", err) } return &deployer.DeployResult{}, nil } func createSDKClient(accessKeyId, accessKeySecret, region string) (*tos.ClientV2, error) { endpoint := fmt.Sprintf("tos-%s.volces.com", region) client, err := tos.NewClientV2( endpoint, tos.WithRegion(region), tos.WithCredentials(tos.NewStaticCredentials(accessKeyId, accessKeySecret)), ) if err != nil { return nil, err } return client, nil } ================================================ FILE: pkg/core/deployer/providers/volcengine-tos/volcengine_tos_test.go ================================================ package volcenginetos_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-tos" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fRegion string fBucket string fDomain string ) func init() { argsPrefix := "VOLCENGINETOS_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fBucket, argsPrefix+"BUCKET", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./volcengine_tos_test.go -args \ --VOLCENGINETOS_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --VOLCENGINETOS_INPUTKEYPATH="/path/to/your-input-key.pem" \ --VOLCENGINETOS_ACCESSKEYID="your-access-key-id" \ --VOLCENGINETOS_ACCESSKEYSECRET="your-access-key-secret" \ --VOLCENGINETOS_REGION="cn-beijing" \ --VOLCENGINETOS_BUCKET="your-tos-bucket" \ --VOLCENGINETOS_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("BUCKET: %v", fBucket), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Region: fRegion, Bucket: fBucket, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/volcengine-vod/consts.go ================================================ package volcenginevod const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" // 匹配模式:通配符匹配。 DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" // 匹配模式:证书 SAN 匹配。 DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" ) const ( // 域名类型:点播加速域名。 DOMAIN_TYPE_PLAY = "play" // 域名类型:封面加速域名。 DOMAIN_TYPE_IMAGE = "image" ) ================================================ FILE: pkg/core/deployer/providers/volcengine-vod/volcengine_vod.go ================================================ package volcenginevod import ( "context" "errors" "fmt" "log/slog" "strings" "github.com/samber/lo" vevod "github.com/volcengine/volc-sdk-golang/service/vod" vevodbusiness "github.com/volcengine/volc-sdk-golang/service/vod/models/business" vevodrequest "github.com/volcengine/volc-sdk-golang/service/vod/models/request" ve "github.com/volcengine/volcengine-go-sdk/volcengine" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-certcenter" "github.com/certimate-go/certimate/pkg/core/deployer" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) type DeployerConfig struct { // 火山引擎 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 火山引擎 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 点播空间名称。 SpaceName string `json:"spaceName"` // 域名匹配模式。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 点播域名类型。 DomainType string `json:"domainType"` // 点播加速域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *vevod.Vod sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client := vevod.NewInstance() client.SetAccessKey(config.AccessKeyId) client.SetSecretKey(config.AccessKeySecret) pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名 domains := make([]string, 0) switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } domains = append(domains, d.config.Domain) } case DOMAIN_MATCH_PATTERN_WILDCARD: { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } if strings.HasPrefix(d.config.Domain, "*.") { domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return xcerthostname.IsMatch(d.config.Domain, domain) }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by wildcard") } } else { domains = append(domains, d.config.Domain) } } case DOMAIN_MATCH_PATTERN_CERTSAN: { certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, err } domainCandidates, err := d.getAllDomains(ctx) if err != nil { return nil, err } domains = lo.Filter(domainCandidates, func(domain string, _ int) bool { return certX509.VerifyHostname(domain) == nil }) if len(domains) == 0 { return nil, errors.New("could not find any domains matched by certificate") } } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 遍历更新域名证书 if len(domains) == 0 { d.logger.Info("no vod domains to deploy") } else { d.logger.Info("found vod domains to deploy", slog.Any("domains", domains)) var errs []error for _, domain := range domains { select { case <-ctx.Done(): return nil, ctx.Err() default: if err := d.updateDomainCertificate(ctx, domain, upres.CertId); err != nil { errs = append(errs, err) } } } if len(errs) > 0 { return nil, errors.Join(errs...) } } return &deployer.DeployResult{}, nil } func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) { domains := make([]string, 0) // 获取空间域名列表 // REF: https://www.volcengine.com/docs/4/106062 listDomainDetailOffset := 0 listDomainDetailLimit := 1000 for { select { case <-ctx.Done(): return nil, ctx.Err() default: } listDomainReq := &vevodrequest.VodListDomainRequest{ SpaceName: d.config.SpaceName, DomainType: d.config.DomainType, SourceStationType: 1, Offset: int32(listDomainDetailOffset), Limit: int32(listDomainDetailLimit), } listDomainResp, _, err := d.sdkClient.ListDomain(listDomainReq) d.logger.Debug("sdk request 'vod.ListDomain'", slog.Any("request", listDomainReq), slog.Any("response", listDomainResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'vod.ListDomain': %w", err) } if listDomainResp.Result == nil { break } var domainInstances []*vevodbusiness.VodDomainInstanceInfo switch d.config.DomainType { case DOMAIN_TYPE_PLAY: domainInstances = listDomainResp.GetResult().GetPlayInstanceInfo().GetByteInstances() case DOMAIN_TYPE_IMAGE: domainInstances = listDomainResp.GetResult().GetImageInstanceInfo().GetByteInstances() default: return nil, fmt.Errorf("unsupported domain type: '%s'", d.config.DomainType) } for _, domainInstance := range domainInstances { if domainInstance.Domains == nil { continue } for _, domainItem := range domainInstance.Domains { domains = append(domains, domainItem.Domain) } } if listDomainResp.Result.Total <= int64(listDomainDetailOffset+listDomainDetailLimit) { break } listDomainDetailOffset += listDomainDetailLimit } return domains, nil } func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error { // 更新域名配置 // REF: https://www.volcengine.com/docs/4/1317310 updateDomainConfigReq := &vevodrequest.VodUpdateDomainConfigRequest{ SpaceName: d.config.SpaceName, DomainType: d.config.DomainType, Domain: domain, Config: &vevodbusiness.VodDomainConfig{ HTTPS: &vevodbusiness.HTTPS{ Switch: ve.Bool(true), CertInfo: &vevodbusiness.CertInfo{ CertId: &cloudCertId, }, }, }, } updateDomainConfigResp, _, err := d.sdkClient.UpdateDomainConfig(updateDomainConfigReq) d.logger.Debug("sdk request 'vod.UpdateDomainConfig'", slog.Any("request", updateDomainConfigReq), slog.Any("response", updateDomainConfigResp)) if err != nil { return err } return nil } ================================================ FILE: pkg/core/deployer/providers/volcengine-vod/volcengine_vod_test.go ================================================ package volcenginevod_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-vod" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fSpaceName string fDomainType string fDomain string ) func init() { argsPrefix := "VOLCENGINEVOD_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fSpaceName, argsPrefix+"SPACENAME", "", "") flag.StringVar(&fDomainType, argsPrefix+"DOMAINTYPE", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./volcengine_vod_test.go -args \ --VOLCENGINEVOD_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --VOLCENGINEVOD_INPUTKEYPATH="/path/to/your-input-key.pem" \ --VOLCENGINEVOD_ACCESSKEYID="your-access-key-id" \ --VOLCENGINEVOD_ACCESSKEYSECRET="your-access-key-secret" \ --VOLCENGINEVOD_SPACENAME="vod-space-name" \ --VOLCENGINEVOD_DOMAINTYPE="play" \ --VOLCENGINEVOD_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("SPACENAME: %v", fSpaceName), fmt.Sprintf("DOMAINTYPE: %v", fDomainType), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, DomainMatchPattern: provider.DOMAIN_MATCH_PATTERN_EXACT, SpaceName: fSpaceName, DomainType: fDomainType, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/volcengine-waf/consts.go ================================================ package volcenginewaf const ( // 接入模式:CNAME 接入。 ACCESS_MODE_CNAME = "cname" ) ================================================ FILE: pkg/core/deployer/providers/volcengine-waf/internal/client.go ================================================ package internal import ( "github.com/volcengine/volcengine-go-sdk/service/waf" "github.com/volcengine/volcengine-go-sdk/volcengine" "github.com/volcengine/volcengine-go-sdk/volcengine/client" "github.com/volcengine/volcengine-go-sdk/volcengine/client/metadata" "github.com/volcengine/volcengine-go-sdk/volcengine/corehandlers" "github.com/volcengine/volcengine-go-sdk/volcengine/request" "github.com/volcengine/volcengine-go-sdk/volcengine/signer/volc" "github.com/volcengine/volcengine-go-sdk/volcengine/volcenginequery" ) // This is a partial copy of https://github.com/volcengine/volcengine-go-sdk/blob/master/service/waf/service_waf.go // to lightweight the vendor packages in the built binary. type WafClient struct { *client.Client } func NewWafClient(p client.ConfigProvider, cfgs ...*volcengine.Config) *WafClient { c := p.ClientConfig(waf.EndpointsID, cfgs...) return newDcdnClient(*c.Config, c.Handlers, c.Endpoint, c.SigningRegion, c.SigningName) } func newDcdnClient(cfg volcengine.Config, handlers request.Handlers, endpoint, signingRegion, signingName string) *WafClient { svc := &WafClient{ Client: client.New( cfg, metadata.ClientInfo{ ServiceName: waf.ServiceName, ServiceID: waf.ServiceID, SigningName: signingName, SigningRegion: signingRegion, Endpoint: endpoint, APIVersion: "2023-12-25", }, handlers, ), } svc.Handlers.Build.PushBackNamed(corehandlers.SDKVersionUserAgentHandler) svc.Handlers.Build.PushBackNamed(corehandlers.AddHostExecEnvUserAgentHandler) svc.Handlers.Sign.PushBackNamed(volc.SignRequestHandler) svc.Handlers.Build.PushBackNamed(volcenginequery.BuildHandler) svc.Handlers.Unmarshal.PushBackNamed(volcenginequery.UnmarshalHandler) svc.Handlers.UnmarshalMeta.PushBackNamed(volcenginequery.UnmarshalMetaHandler) svc.Handlers.UnmarshalError.PushBackNamed(volcenginequery.UnmarshalErrorHandler) return svc } func (c *WafClient) newRequest(op *request.Operation, params, data interface{}) *request.Request { req := c.NewRequest(op, params, data) return req } func (c *WafClient) UpdateDomain(input *waf.UpdateDomainInput) (*waf.UpdateDomainOutput, error) { req, out := c.UpdateDomainRequest(input) return out, req.Send() } func (c *WafClient) UpdateDomainRequest(input *waf.UpdateDomainInput) (req *request.Request, output *waf.UpdateDomainOutput) { op := &request.Operation{ Name: "UpdateDomain", HTTPMethod: "POST", HTTPPath: "/", } if input == nil { input = &waf.UpdateDomainInput{} } output = &waf.UpdateDomainOutput{} req = c.newRequest(op, input, output) req.HTTPRequest.Header.Set("Content-Type", "application/json; charset=utf-8") return } func (c *WafClient) ListDomain(input *waf.ListDomainInput) (*waf.ListDomainOutput, error) { req, out := c.ListDomainRequest(input) return out, req.Send() } func (c *WafClient) ListDomainRequest(input *waf.ListDomainInput) (req *request.Request, output *waf.ListDomainOutput) { op := &request.Operation{ Name: "ListDomain", HTTPMethod: "POST", HTTPPath: "/", } if input == nil { input = &waf.ListDomainInput{} } output = &waf.ListDomainOutput{} req = c.newRequest(op, input, output) req.HTTPRequest.Header.Set("Content-Type", "application/json; charset=utf-8") return } ================================================ FILE: pkg/core/deployer/providers/volcengine-waf/volcengine_waf.go ================================================ package volcenginewaf import ( "context" "errors" "fmt" "log/slog" "strings" "github.com/samber/lo" vewaf "github.com/volcengine/volcengine-go-sdk/service/waf" ve "github.com/volcengine/volcengine-go-sdk/volcengine" vesession "github.com/volcengine/volcengine-go-sdk/volcengine/session" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/volcengine-certcenter" "github.com/certimate-go/certimate/pkg/core/deployer" "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-waf/internal" ) type DeployerConfig struct { // 火山引擎 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 火山引擎 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 火山引擎地域。 Region string `json:"region"` // WAF 接入模式。 AccessMode string `json:"accessMode"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *internal.WafClient sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret, config.Region) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, Region: config.Region, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, nil } func (d *Deployer) SetLogger(logger *slog.Logger) { if logger == nil { d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } d.sdkCertmgr.SetLogger(logger) } func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 根据接入方式决定部署方式 switch d.config.AccessMode { case ACCESS_MODE_CNAME: if err := d.deployWithCNAME(ctx, upres.CertId); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported access mode '%s'", d.config.AccessMode) } return &deployer.DeployResult{}, nil } func (d *Deployer) deployWithCNAME(ctx context.Context, cloudCertId string) error { if d.config.Domain == "" { return errors.New("config `domain` is required") } // 查询云 WAF 实例防护网站信息 // REF: https://www.volcengine.com/docs/6511/1214827 listDomainReq := &vewaf.ListDomainInput{ Region: ve.String(d.config.Region), Domain: ve.String(d.config.Domain), AccurateQuery: ve.Int32(1), Page: ve.Int32(1), PageSize: ve.Int32(1), } listDomainResp, err := d.sdkClient.ListDomain(listDomainReq) d.logger.Debug("sdk request 'waf.ListDomain'", slog.Any("request", listDomainReq), slog.Any("response", listDomainResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'waf.ListDomain': %w", err) } else if len(listDomainResp.Data) == 0 { return fmt.Errorf("could not find domain '%s'", d.config.Domain) } // 更新云 WAF 实例的防护网站信息 // REF: https://www.volcengine.com/docs/6511/1214835 domainInfo := listDomainResp.Data[0] updateDomainReq := &vewaf.UpdateDomainInput{ Region: ve.String(d.config.Region), Domain: ve.String(d.config.Domain), AccessMode: ve.Int32(10), Protocols: ve.StringSlice([]string{"HTTP", "HTTPS"}), ProtocolPorts: &vewaf.ProtocolPortsForUpdateDomainInput{ HTTP: ve.Int32Slice([]int32{80}), HTTPS: ve.Int32Slice([]int32{443}), }, VolcCertificateID: ve.String(cloudCertId), CertificatePlatform: ve.String("certificate-service"), } if domainInfo.Protocols != nil { protocols := strings.Split(ve.StringValue(domainInfo.Protocols), ",") if !lo.Contains(protocols, "HTTPS") { protocols = append(protocols, "HTTPS") } updateDomainReq.Protocols = ve.StringSlice(protocols) } if domainInfo.ProtocolPorts != nil { updateDomainReq.ProtocolPorts.HTTP = domainInfo.ProtocolPorts.HTTP if domainInfo.ProtocolPorts.HTTPS != nil { updateDomainReq.ProtocolPorts.HTTPS = domainInfo.ProtocolPorts.HTTPS } } updateDomainResp, err := d.sdkClient.UpdateDomain(updateDomainReq) d.logger.Debug("sdk request 'waf.UpdateDomain'", slog.Any("request", updateDomainReq), slog.Any("response", updateDomainResp)) if err != nil { return fmt.Errorf("failed to execute sdk request 'waf.UpdateDomain': %w", err) } return nil } func createSDKClient(accessKeyId, accessKeySecret, region string) (*internal.WafClient, error) { config := ve.NewConfig(). WithAkSk(accessKeyId, accessKeySecret). WithRegion(region) session, err := vesession.NewSession(config) if err != nil { return nil, err } client := internal.NewWafClient(session) return client, nil } ================================================ FILE: pkg/core/deployer/providers/volcengine-waf/volcengine_waf_test.go ================================================ package volcenginewaf_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/volcengine-waf" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fRegion string fAccessMode string fDomain string ) func init() { argsPrefix := "VOLCENGINEWAF_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") flag.StringVar(&fAccessMode, argsPrefix+"ACCESSMODE", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./volcengine_waf_test.go -args \ --VOLCENGINEWAF_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --VOLCENGINEWAF_INPUTKEYPATH="/path/to/your-input-key.pem" \ --VOLCENGINEWAF_ACCESSKEYID="your-access-key-id" \ --VOLCENGINEWAF_ACCESSKEYSECRET="your-access-key-secret" \ --VOLCENGINEWAF_REGION="cn-beijing" \ --VOLCENGINEWAF_ACCESSMODE="cname" \ --VOLCENGINEWAF_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("REGION: %v", fRegion), fmt.Sprintf("ACCESSMODE: %v", fAccessMode), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Region: fRegion, AccessMode: fAccessMode, Domain: fDomain, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/wangsu-cdn/consts.go ================================================ package wangsucdn const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" ) ================================================ FILE: pkg/core/deployer/providers/wangsu-cdn/wangsu_cdn.go ================================================ package wangsucdn import ( "context" "errors" "fmt" "log/slog" "strconv" "strings" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/wangsu-certificate" "github.com/certimate-go/certimate/pkg/core/deployer" wangsusdk "github.com/certimate-go/certimate/pkg/sdk3rd/wangsu/cdn" ) type DeployerConfig struct { // 网宿云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 网宿云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 域名匹配模式。暂时只支持精确匹配。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名数组(支持泛域名)。 Domains []string `json:"domains"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *wangsusdk.Client sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, 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) (*deployer.DeployResult, error) { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } // 获取待部署的域名列表 domains := make([]string, 0) switch d.config.DomainMatchPattern { case "", DOMAIN_MATCH_PATTERN_EXACT: { if len(d.config.Domains) == 0 { return nil, errors.New("config `domains` is required") } // "*.example.com" → ".example.com",适配网宿云 CDN 要求的泛域名格式 domains = lo.Map(d.config.Domains, func(domain string, _ int) string { return strings.TrimPrefix(domain, "*") }) } default: return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern) } // 批量修改域名证书配置 // REF: https://www.wangsu.com/document/api-doc/37447 certId, _ := strconv.ParseInt(upres.CertId, 10, 64) batchUpdateCertificateConfigReq := &wangsusdk.BatchUpdateCertificateConfigRequest{ CertificateId: certId, DomainNames: domains, } batchUpdateCertificateConfigResp, err := d.sdkClient.BatchUpdateCertificateConfigWithContext(ctx, batchUpdateCertificateConfigReq) d.logger.Debug("sdk request 'cdn.BatchUpdateCertificateConfig'", slog.Any("request", batchUpdateCertificateConfigReq), slog.Any("response", batchUpdateCertificateConfigResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdn.BatchUpdateCertificateConfig': %w", err) } return &deployer.DeployResult{}, nil } func createSDKClient(accessKeyId, accessKeySecret string) (*wangsusdk.Client, error) { return wangsusdk.NewClient(accessKeyId, accessKeySecret) } ================================================ FILE: pkg/core/deployer/providers/wangsu-cdn/wangsu_cdn_test.go ================================================ package wangsucdn_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/wangsu-cdn" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fDomain string ) func init() { argsPrefix := "WANGSUCDN_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") } /* Shell command to run this test: go test -v ./wangsu_cdn_test.go -args \ --WANGSUCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --WANGSUCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ --WANGSUCDN_ACCESSKEYID="your-access-key-id" \ --WANGSUCDN_ACCESSKEYSECRET="your-access-key-secret" \ --WANGSUCDN_DOMAIN="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("DOMAIN: %v", fDomain), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, Domains: []string{fDomain}, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/wangsu-cdnpro/consts.go ================================================ package wangsucdnpro const ( // 匹配模式:精确匹配。 DOMAIN_MATCH_PATTERN_EXACT = "exact" ) ================================================ FILE: pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go ================================================ package wangsucdnpro import ( "bytes" "context" "crypto/aes" "crypto/cipher" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "errors" "fmt" "log/slog" "regexp" "strconv" "time" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core/deployer" wangsucdn "github.com/certimate-go/certimate/pkg/sdk3rd/wangsu/cdnpro" xwait "github.com/certimate-go/certimate/pkg/utils/wait" ) type DeployerConfig struct { // 网宿云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 网宿云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 网宿云 API Key。 ApiKey string `json:"apiKey"` // 网宿云环境。 Environment string `json:"environment"` // 域名匹配模式。暂时只支持精确匹配。 // 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。 DomainMatchPattern string `json:"domainMatchPattern,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` // 证书 ID。 // 选填。零值时表示新建证书;否则表示更新证书。 CertificateId string `json:"certificateId,omitempty"` // Webhook ID。 // 选填。 WebhookId string `json:"webhookId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *wangsucdn.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret) 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) (*deployer.DeployResult, error) { if d.config.Domain == "" { return nil, errors.New("config `domain` is required") } // 查询已部署加速域名的详情 getHostnameDetailResp, err := d.sdkClient.GetHostnameDetailWithContext(ctx, d.config.Domain) d.logger.Debug("sdk request 'cdnpro.GetHostnameDetail'", slog.String("hostname", d.config.Domain), slog.Any("response", getHostnameDetailResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdnpro.GetHostnameDetail': %w", err) } // 生成网宿云证书参数 encryptedPrivateKey, err := encryptPrivateKey(privkeyPEM, d.config.ApiKey, time.Now().Unix()) if err != nil { return nil, fmt.Errorf("failed to encrypt private key: %w", err) } certificateNewVersionInfo := &wangsucdn.CertificateVersionInfo{ PrivateKey: lo.ToPtr(encryptedPrivateKey), Certificate: lo.ToPtr(certPEM), } // 网宿云证书 URL 中包含证书 ID 及版本号 // 格式: // http://open.chinanetcenter.com/cdn/certificates/5dca2205f9e9cc0001df7b33 // http://open.chinanetcenter.com/cdn/certificates/329f12c1fe6708c23c31e91f/versions/5 var wangsuCertUrl string var wangsuCertId string var wangsuCertVer int32 // 如果原证书 ID 为空,则创建证书;否则更新证书。 timestamp := time.Now().Unix() if d.config.CertificateId == "" { // 创建证书 createCertificateReq := &wangsucdn.CreateCertificateRequest{ Timestamp: timestamp, Name: lo.ToPtr(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())), AutoRenew: lo.ToPtr("Off"), NewVersion: certificateNewVersionInfo, } createCertificateResp, err := d.sdkClient.CreateCertificateWithContext(ctx, createCertificateReq) d.logger.Debug("sdk request 'cdnpro.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdnpro.CreateCertificate': %w", err) } wangsuCertUrl = createCertificateResp.CertificateLocation d.logger.Info("ssl certificate uploaded", slog.Any("certUrl", wangsuCertUrl)) wangsuCertIdMatches := regexp.MustCompile(`/certificates/([a-zA-Z0-9-]+)`).FindStringSubmatch(wangsuCertUrl) if len(wangsuCertIdMatches) > 1 { wangsuCertId = wangsuCertIdMatches[1] } wangsuCertVer = 1 } else { // 更新证书 updateCertificateReq := &wangsucdn.UpdateCertificateRequest{ Timestamp: timestamp, Name: lo.ToPtr(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())), AutoRenew: lo.ToPtr("Off"), NewVersion: certificateNewVersionInfo, } updateCertificateResp, err := d.sdkClient.UpdateCertificateWithContext(ctx, d.config.CertificateId, updateCertificateReq) d.logger.Debug("sdk request 'cdnpro.CreateCertificate'", slog.Any("certificateId", d.config.CertificateId), slog.Any("request", updateCertificateReq), slog.Any("response", updateCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdnpro.UpdateCertificate': %w", err) } wangsuCertUrl = updateCertificateResp.CertificateLocation d.logger.Info("ssl certificate uploaded", slog.Any("certUrl", wangsuCertUrl)) wangsuCertIdMatches := regexp.MustCompile(`/certificates/([a-zA-Z0-9-]+)`).FindStringSubmatch(wangsuCertUrl) if len(wangsuCertIdMatches) > 1 { wangsuCertId = wangsuCertIdMatches[1] } wangsuCertVerMatches := regexp.MustCompile(`/versions/(\d+)`).FindStringSubmatch(wangsuCertUrl) if len(wangsuCertVerMatches) > 1 { n, _ := strconv.ParseInt(wangsuCertVerMatches[1], 10, 32) wangsuCertVer = int32(n) } } // 创建部署任务 // REF: https://www.wangsu.com/document/api-doc/27034 var wangsuTaskId string createDeploymentTaskReq := &wangsucdn.CreateDeploymentTaskRequest{ Name: lo.ToPtr(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())), Target: lo.ToPtr(d.config.Environment), Actions: &[]wangsucdn.DeploymentTaskActionInfo{ { Action: lo.ToPtr("deploy_cert"), CertificateId: lo.ToPtr(wangsuCertId), Version: lo.ToPtr(wangsuCertVer), }, }, } if d.config.WebhookId != "" { createDeploymentTaskReq.Webhook = lo.ToPtr(d.config.WebhookId) } createDeploymentTaskResp, err := d.sdkClient.CreateDeploymentTaskWithContext(ctx, createDeploymentTaskReq) d.logger.Debug("sdk request 'cdnpro.CreateCertificate'", slog.Any("request", createDeploymentTaskReq), slog.Any("response", createDeploymentTaskResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'cdnpro.CreateDeploymentTask': %w", err) } else { wangsuTaskMatches := regexp.MustCompile(`/deploymentTasks/([a-zA-Z0-9-]+)`).FindStringSubmatch(createDeploymentTaskResp.DeploymentTaskLocation) if len(wangsuTaskMatches) > 1 { wangsuTaskId = wangsuTaskMatches[1] } } // 获取部署任务详细信息,等待任务状态变更 // REF: https://www.wangsu.com/document/api-doc/27038 if _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) { getDeploymentTaskDetailResp, err := d.sdkClient.GetDeploymentTaskDetailWithContext(ctx, wangsuTaskId) d.logger.Info("sdk request 'cdnpro.GetDeploymentTaskDetail'", slog.Any("taskId", wangsuTaskId), slog.Any("response", getDeploymentTaskDetailResp)) if err != nil { return false, fmt.Errorf("failed to execute sdk request 'cdnpro.GetDeploymentTaskDetail': %w", err) } if getDeploymentTaskDetailResp.Status == "failed" { return false, fmt.Errorf("unexpected wangsu deployment task status") } else if getDeploymentTaskDetailResp.Status == "succeeded" || getDeploymentTaskDetailResp.FinishTime != "" { return true, nil } d.logger.Info(fmt.Sprintf("waiting for wangsu deployment task completion (current status: %s) ...", getDeploymentTaskDetailResp.Status)) return false, nil }, time.Second*5); err != nil { return nil, err } return &deployer.DeployResult{}, nil } func createSDKClient(accessKeyId, accessKeySecret string) (*wangsucdn.Client, error) { return wangsucdn.NewClient(accessKeyId, accessKeySecret) } func encryptPrivateKey(privkeyPEM string, apiKey string, timestamp int64) (string, error) { date := time.Unix(timestamp, 0).UTC() dateStr := date.Format("Mon, 02 Jan 2006 15:04:05 GMT") h := hmac.New(sha256.New, []byte(apiKey)) h.Write([]byte(dateStr)) aesivkey := h.Sum(nil) aesivkeyHex := hex.EncodeToString(aesivkey) if len(aesivkeyHex) != 64 { return "", fmt.Errorf("invalid hmac length: %d", len(aesivkeyHex)) } ivHex := aesivkeyHex[:32] keyHex := aesivkeyHex[32:64] iv, err := hex.DecodeString(ivHex) if err != nil { return "", fmt.Errorf("failed to decode iv: %w", err) } key, err := hex.DecodeString(keyHex) if err != nil { return "", fmt.Errorf("failed to decode key: %w", err) } block, err := aes.NewCipher(key) if err != nil { return "", err } plainBytes := []byte(privkeyPEM) padlen := aes.BlockSize - len(plainBytes)%aes.BlockSize if padlen > 0 { paddata := bytes.Repeat([]byte{byte(padlen)}, padlen) plainBytes = append(plainBytes, paddata...) } encBytes := make([]byte, len(plainBytes)) mode := cipher.NewCBCEncrypter(block, iv) mode.CryptBlocks(encBytes, plainBytes) return base64.StdEncoding.EncodeToString(encBytes), nil } ================================================ FILE: pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro_test.go ================================================ package wangsucdnpro_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/wangsu-cdnpro" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fApiKey string fEnvironment string fDomain string fCertificateId string fWebhookId string ) func init() { argsPrefix := "WANGSUCDNPRO_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") flag.StringVar(&fEnvironment, argsPrefix+"ENVIRONMENT", "production", "") flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "") flag.StringVar(&fWebhookId, argsPrefix+"WEBHOOKID", "", "") } /* Shell command to run this test: go test -v ./wangsu_cdnpro_test.go -args \ --WANGSUCDNPRO_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --WANGSUCDNPRO_INPUTKEYPATH="/path/to/your-input-key.pem" \ --WANGSUCDNPRO_ACCESSKEYID="your-access-key-id" \ --WANGSUCDNPRO_ACCESSKEYSECRET="your-access-key-secret" \ --WANGSUCDNPRO_APIKEY="your-api-key" \ --WANGSUCDNPRO_ENVIRONMENT="production" \ --WANGSUCDNPRO_DOMAIN="example.com" \ --WANGSUCDNPRO_CERTIFICATEID="your-certificate-id" \ --WANGSUCDNPRO_WEBHOOKID="your-webhook-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("APIKEY: %v", fApiKey), fmt.Sprintf("ENVIRONMENT: %v", fEnvironment), fmt.Sprintf("DOMAIN: %v", fDomain), fmt.Sprintf("CERTIFICATEID: %v", fCertificateId), fmt.Sprintf("WEBHOOKID: %v", fWebhookId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, ApiKey: fApiKey, Environment: fEnvironment, Domain: fDomain, CertificateId: fCertificateId, WebhookId: fWebhookId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/wangsu-certificate/wangsu_certificate.go ================================================ package wangsucertificate import ( "context" "errors" "fmt" "log/slog" "github.com/certimate-go/certimate/pkg/core/certmgr" mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/wangsu-certificate" "github.com/certimate-go/certimate/pkg/core/deployer" wangsusdk "github.com/certimate-go/certimate/pkg/sdk3rd/wangsu/certificate" ) type DeployerConfig struct { // 网宿云 AccessKeyId。 AccessKeyId string `json:"accessKeyId"` // 网宿云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` // 证书 ID。 // 选填。零值时表示新建证书;否则表示更新证书。 CertificateId string `json:"certificateId,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger sdkClient *wangsusdk.Client sdkCertmgr certmgr.Provider } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client, err := createSDKClient(config.AccessKeyId, config.AccessKeySecret) if err != nil { return nil, fmt.Errorf("could not create client: %w", err) } pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, }) if err != nil { return nil, fmt.Errorf("could not create certmgr: %w", err) } return &Deployer{ config: config, logger: slog.Default(), sdkClient: client, sdkCertmgr: pcertmgr, }, 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) (*deployer.DeployResult, error) { if d.config.CertificateId == "" { // 上传证书 upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to upload certificate file: %w", err) } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } } else { // 替换证书 opres, err := d.sdkCertmgr.Replace(ctx, d.config.CertificateId, certPEM, privkeyPEM) if err != nil { return nil, fmt.Errorf("failed to replace certificate file: %w", err) } else { d.logger.Info("ssl certificate replaced", slog.Any("result", opres)) } } return &deployer.DeployResult{}, nil } func createSDKClient(accessKeyId, accessKeySecret string) (*wangsusdk.Client, error) { return wangsusdk.NewClient(accessKeyId, accessKeySecret) } ================================================ FILE: pkg/core/deployer/providers/wangsu-certificate/wangsu_certificate_test.go ================================================ package wangsucertificate_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/wangsu-certificate" ) var ( fInputCertPath string fInputKeyPath string fAccessKeyId string fAccessKeySecret string fCertificateId string ) func init() { argsPrefix := "WANGSUCERTIFICATE_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "") } /* Shell command to run this test: go test -v ./wangsu_certificate_test.go -args \ --WANGSUCERTIFICATE_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --WANGSUCERTIFICATE_INPUTKEYPATH="/path/to/your-input-key.pem" \ --WANGSUCERTIFICATE_ACCESSKEYID="your-access-key-id" \ --WANGSUCERTIFICATE_ACCESSKEYSECRET="your-access-key-secret" \ --WANGSUCERTIFICATE_CERTIFICATEID="your-certificate-id" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), fmt.Sprintf("CERTIFICATEID: %v", fCertificateId), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ AccessKeyId: fAccessKeyId, AccessKeySecret: fAccessKeySecret, CertificateId: fCertificateId, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/deployer/providers/webhook/webhook.go ================================================ package webhook import ( "context" "crypto/tls" "encoding/json" "errors" "fmt" "log/slog" "net/http" "net/url" "strings" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/pkg/core/deployer" xcert "github.com/certimate-go/certimate/pkg/utils/cert" xcertx509 "github.com/certimate-go/certimate/pkg/utils/cert/x509" ) type DeployerConfig struct { // Webhook URL。 WebhookUrl string `json:"webhookUrl"` // Webhook 回调数据(application/json 或 application/x-www-form-urlencoded 格式)。 WebhookData string `json:"webhookData,omitempty"` // 请求谓词。 // 零值时默认值 "POST"。 Method string `json:"method,omitempty"` // 请求标头。 Headers map[string]string `json:"headers,omitempty"` // 请求超时(单位:秒)。 // 零值时默认值 30。 Timeout int `json:"timeout,omitempty"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type Deployer struct { config *DeployerConfig logger *slog.Logger httpClient *resty.Client } var _ deployer.Provider = (*Deployer)(nil) func NewDeployer(config *DeployerConfig) (*Deployer, error) { if config == nil { return nil, errors.New("the configuration of the deployer provider is nil") } client := resty.New(). SetTimeout(30 * time.Second). SetRetryCount(3). SetRetryWaitTime(5 * time.Second) if config.Timeout > 0 { client.SetTimeout(time.Duration(config.Timeout) * time.Second) } if config.AllowInsecureConnections { client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) } return &Deployer{ config: config, logger: slog.Default(), httpClient: 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) (*deployer.DeployResult, error) { // 解析证书内容 certX509, err := xcert.ParseCertificateFromPEM(certPEM) if err != nil { return nil, fmt.Errorf("failed to parse x509: %w", err) } // 提取服务器证书和中间证书 serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM) if err != nil { return nil, fmt.Errorf("failed to extract certs: %w", err) } // 处理 Webhook URL webhookUrl, err := url.Parse(d.config.WebhookUrl) if err != nil { return nil, fmt.Errorf("failed to parse webhook url: %w", err) } else if webhookUrl.Scheme != "http" && webhookUrl.Scheme != "https" { return nil, fmt.Errorf("unsupported webhook url scheme '%s'", webhookUrl.Scheme) } // 处理 Webhook 请求谓词 webhookMethod := strings.ToUpper(d.config.Method) if webhookMethod == "" { webhookMethod = http.MethodPost } else if webhookMethod != http.MethodGet && webhookMethod != http.MethodPost && webhookMethod != http.MethodPut && webhookMethod != http.MethodPatch && webhookMethod != http.MethodDelete { return nil, fmt.Errorf("unsupported webhook request method '%s'", webhookMethod) } // 处理 Webhook 请求标头 webhookHeaders := make(http.Header) for k, v := range d.config.Headers { webhookHeaders.Set(k, v) } // 处理 Webhook 请求内容类型 const CONTENT_TYPE_JSON = "application/json" const CONTENT_TYPE_FORM = "application/x-www-form-urlencoded" const CONTENT_TYPE_MULTIPART = "multipart/form-data" webhookContentType := webhookHeaders.Get("Content-Type") if webhookContentType == "" { webhookContentType = CONTENT_TYPE_JSON webhookHeaders.Set("Content-Type", CONTENT_TYPE_JSON) } else if strings.HasPrefix(webhookContentType, CONTENT_TYPE_JSON) && strings.HasPrefix(webhookContentType, CONTENT_TYPE_FORM) && strings.HasPrefix(webhookContentType, CONTENT_TYPE_MULTIPART) { return nil, fmt.Errorf("unsupported webhook content type '%s'", webhookContentType) } // 处理 Webhook 请求数据 var webhookData interface{} if d.config.WebhookData == "" { webhookData = map[string]string{ "name": strings.Join(xcertx509.GetSubjectAltNames(certX509), ";"), "cert": certPEM, "privkey": privkeyPEM, } } else { err = json.Unmarshal([]byte(d.config.WebhookData), &webhookData) if err != nil { return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err) } if webhookMethod == http.MethodGet || webhookContentType == CONTENT_TYPE_FORM || webhookContentType == CONTENT_TYPE_MULTIPART { temp := make(map[string]string) jsonb, err := json.Marshal(webhookData) if err != nil { return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err) } else if err := json.Unmarshal(jsonb, &temp); err != nil { return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err) } else { webhookData = temp } } } // 替换变量值 webhookUrl.Path = strings.ReplaceAll(webhookUrl.Path, "${CERTIMATE_DEPLOYER_COMMONNAME}", url.PathEscape(xcertx509.GetSubjectCommonName(certX509))) replaceJsonValueRecursively(webhookData, "${CERTIMATE_DEPLOYER_COMMONNAME}", xcertx509.GetSubjectCommonName(certX509)) replaceJsonValueRecursively(webhookData, "${CERTIMATE_DEPLOYER_SUBJECTALTNAMES}", strings.Join(xcertx509.GetSubjectAltNames(certX509), ";")) replaceJsonValueRecursively(webhookData, "${CERTIMATE_DEPLOYER_CERTIFICATE}", certPEM) replaceJsonValueRecursively(webhookData, "${CERTIMATE_DEPLOYER_CERTIFICATE_SERVER}", serverCertPEM) replaceJsonValueRecursively(webhookData, "${CERTIMATE_DEPLOYER_CERTIFICATE_INTERMEDIA}", intermediaCertPEM) replaceJsonValueRecursively(webhookData, "${CERTIMATE_DEPLOYER_PRIVATEKEY}", privkeyPEM) // 兼容旧版变量 webhookUrl.Path = strings.ReplaceAll(webhookUrl.Path, "${DOMAIN}", url.PathEscape(certX509.Subject.CommonName)) replaceJsonValueRecursively(webhookData, "${DOMAIN}", certX509.Subject.CommonName) replaceJsonValueRecursively(webhookData, "${DOMAINS}", strings.Join(certX509.DNSNames, ";")) replaceJsonValueRecursively(webhookData, "${CERTIFICATE}", certPEM) replaceJsonValueRecursively(webhookData, "${SERVER_CERTIFICATE}", serverCertPEM) replaceJsonValueRecursively(webhookData, "${INTERMEDIA_CERTIFICATE}", intermediaCertPEM) replaceJsonValueRecursively(webhookData, "${PRIVATE_KEY}", privkeyPEM) // 生成请求 // 其中 GET 请求需转换为查询参数 req := d.httpClient.R().SetHeaderMultiValues(webhookHeaders) req.URL = webhookUrl.String() req.Method = webhookMethod if webhookMethod == http.MethodGet { req.SetQueryParams(webhookData.(map[string]string)) } else { switch webhookContentType { case CONTENT_TYPE_JSON: req.SetBody(webhookData) case CONTENT_TYPE_FORM: req.SetFormData(webhookData.(map[string]string)) case CONTENT_TYPE_MULTIPART: req.SetMultipartFormData(webhookData.(map[string]string)) } } // 发送请求 resp, err := req.Send() if err != nil { return nil, fmt.Errorf("failed to send webhook request: %w", err) } else if resp.IsError() { return nil, fmt.Errorf("unexpected webhook response status code: %d", resp.StatusCode()) } d.logger.Debug("webhook responded", slog.Any("response", resp.String())) return &deployer.DeployResult{}, nil } func replaceJsonValueRecursively(data interface{}, oldStr, newStr string) interface{} { switch v := data.(type) { case map[string]any: for k, val := range v { v[k] = replaceJsonValueRecursively(val, oldStr, newStr) } case []any: for i, val := range v { v[i] = replaceJsonValueRecursively(val, oldStr, newStr) } case string: return strings.ReplaceAll(v, oldStr, newStr) } return data } ================================================ FILE: pkg/core/deployer/providers/webhook/webhook_test.go ================================================ package webhook_test import ( "context" "flag" "fmt" "os" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/webhook" ) var ( fInputCertPath string fInputKeyPath string fWebhookUrl string fWebhookContentType string fWebhookData string ) func init() { argsPrefix := "WEBHOOK_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") flag.StringVar(&fWebhookUrl, argsPrefix+"URL", "", "") flag.StringVar(&fWebhookContentType, argsPrefix+"CONTENTTYPE", "application/json", "") flag.StringVar(&fWebhookData, argsPrefix+"DATA", "", "") } /* Shell command to run this test: go test -v ./webhook_test.go -args \ --WEBHOOK_INPUTCERTPATH="/path/to/your-input-cert.pem" \ --WEBHOOK_INPUTKEYPATH="/path/to/your-input-key.pem" \ --WEBHOOK_URL="https://example.com/your-webhook-url" \ --WEBHOOK_CONTENTTYPE="application/json" \ --WEBHOOK_DATA="{\"certificate\":\"${CERTIFICATE}\",\"privateKey\":\"${PRIVATE_KEY}\"}" */ func TestDeploy(t *testing.T) { flag.Parse() t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), fmt.Sprintf("WEBHOOKURL: %v", fWebhookUrl), fmt.Sprintf("WEBHOOKCONTENTTYPE: %v", fWebhookContentType), fmt.Sprintf("WEBHOOKDATA: %v", fWebhookData), }, "\n")) provider, err := provider.NewDeployer(&provider.DeployerConfig{ WebhookUrl: fWebhookUrl, WebhookData: fWebhookData, Method: "POST", Headers: map[string]string{ "Content-Type": fWebhookContentType, }, AllowInsecureConnections: true, }) if err != nil { t.Errorf("err: %+v", err) return } fInputCertData, _ := os.ReadFile(fInputCertPath) fInputKeyData, _ := os.ReadFile(fInputKeyPath) res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/notifier/provider.go ================================================ package notifier import ( "context" "log/slog" ) // 表示定义消息通知器的抽象类型接口。 type Provider interface { // 设置日志记录器。 // // 入参: // - logger:日志记录器实例。 SetLogger(logger *slog.Logger) // 发送通知。 // // 入参: // - ctx:上下文。 // - subject:通知主题。 // - message:通知内容。 // // 出参: // - res:发送结果。 // - err: 错误。 Notify(ctx context.Context, subject, message string) (_res *NotifyResult, _err error) } // 表示通知发送结果的数据结构。 type NotifyResult struct { ExtendedData map[string]any `json:"extendedData,omitempty"` } ================================================ FILE: pkg/core/notifier/providers/dingtalkbot/dingtalkbot.go ================================================ package dingtalkbot import ( "context" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "errors" "fmt" "log/slog" "net/http" "net/url" "strings" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/pkg/core/notifier" ) type NotifierConfig struct { // 钉钉机器人的 Webhook 地址。 WebhookUrl string `json:"webhookUrl"` // 钉钉机器人的 Secret。 Secret string `json:"secret"` // 自定义消息数据。 // 选填。 CustomPayload string `json:"customPayload,omitempty"` } type Notifier struct { config *NotifierConfig logger *slog.Logger httpClient *resty.Client } var _ notifier.Provider = (*Notifier)(nil) func NewNotifier(config *NotifierConfig) (*Notifier, error) { if config == nil { return nil, errors.New("the configuration of the notifier provider is nil") } client := resty.New(). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { if config.Secret != "" { timestamp := fmt.Sprintf("%d", time.Now().UnixMilli()) h := hmac.New(sha256.New, []byte(config.Secret)) h.Write([]byte(fmt.Sprintf("%s\n%s", timestamp, config.Secret))) sign := base64.StdEncoding.EncodeToString(h.Sum(nil)) qs := req.URL.Query() qs.Set("timestamp", timestamp) qs.Set("sign", sign) req.URL.RawQuery = qs.Encode() } return nil }) return &Notifier{ config: config, logger: slog.Default(), httpClient: client, }, nil } func (n *Notifier) SetLogger(logger *slog.Logger) { if logger == nil { n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } } func (n *Notifier) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { webhookUrl, err := url.Parse(n.config.WebhookUrl) if err != nil { return nil, fmt.Errorf("dingtalk api error: invalid webhook url: %w", err) } else { const hostname = "oapi.dingtalk.com" if webhookUrl.Hostname() != hostname { n.logger.Warn(fmt.Sprintf("the webhook url hostname is not '%s', please make sure it is correct", hostname)) } } var webhookData map[string]any if n.config.CustomPayload == "" { webhookData = map[string]any{ "msgtype": "text", "text": map[string]string{ "content": subject + "\n\n" + message, }, } } else { err = json.Unmarshal([]byte(n.config.CustomPayload), &webhookData) if err != nil { return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err) } replaceJsonValueRecursively(webhookData, "${CERTIMATE_NOTIFIER_SUBJECT}", subject) replaceJsonValueRecursively(webhookData, "${CERTIMATE_NOTIFIER_MESSAGE}", message) replaceJsonValueRecursively(webhookData, "${SUBJECT}", subject) replaceJsonValueRecursively(webhookData, "${MESSAGE}", message) } // REF: https://open.dingtalk.com/document/development/custom-robots-send-group-messages var result struct { ErrorCode int `json:"errcode"` ErrorMessage string `json:"errmsg"` } req := n.httpClient.R(). SetContext(ctx). SetBody(webhookData) resp, err := req.Post(webhookUrl.String()) if err != nil { return nil, fmt.Errorf("dingtalk api error: failed to send request: %w", err) } else if resp.IsError() { return nil, fmt.Errorf("dingtalk api error: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } else if err := json.Unmarshal(resp.Body(), &result); err != nil { return nil, fmt.Errorf("dingtalk api error: %w (resp: %s)", err, resp.String()) } else if result.ErrorCode != 0 { return nil, fmt.Errorf("dingtalk api error: errcode='%d', errmsg='%s'", result.ErrorCode, result.ErrorMessage) } return ¬ifier.NotifyResult{}, nil } func replaceJsonValueRecursively(data interface{}, oldStr, newStr string) interface{} { switch v := data.(type) { case map[string]any: for k, val := range v { v[k] = replaceJsonValueRecursively(val, oldStr, newStr) } case []any: for i, val := range v { v[i] = replaceJsonValueRecursively(val, oldStr, newStr) } case []string: for i, s := range v { var val interface{} = s var newVal interface{} = replaceJsonValueRecursively(val, oldStr, newStr) v[i] = newVal.(string) } case string: return strings.ReplaceAll(v, oldStr, newStr) } return data } ================================================ FILE: pkg/core/notifier/providers/dingtalkbot/dingtalkbot_test.go ================================================ package dingtalkbot_test import ( "context" "flag" "fmt" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/notifier/providers/dingtalkbot" ) const ( mockSubject = "test_subject" mockMessage = "test_message" ) var ( fWebhookUrl string fSecret string ) func init() { argsPrefix := "DINGTALKBOT_" flag.StringVar(&fWebhookUrl, argsPrefix+"WEBHOOKURL", "", "") flag.StringVar(&fSecret, argsPrefix+"SECRET", "", "") } /* Shell command to run this test: go test -v ./dingtalkbot_test.go -args \ --DINGTALKBOT_WEBHOOKURL="https://example.com/your-webhook-url" \ --DINGTALKBOT_SECRET="your-secret" */ func TestNotify(t *testing.T) { flag.Parse() t.Run("Notify", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("WEBHOOKURL: %v", fWebhookUrl), fmt.Sprintf("SECRET: %v", fSecret), }, "\n")) provider, err := provider.NewNotifier(&provider.NotifierConfig{ WebhookUrl: fWebhookUrl, Secret: fSecret, }) if err != nil { t.Errorf("err: %+v", err) return } res, err := provider.Notify(context.Background(), mockSubject, mockMessage) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/notifier/providers/discordbot/discordbot.go ================================================ package discordbot import ( "context" "errors" "fmt" "log/slog" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/pkg/core/notifier" ) type NotifierConfig struct { // Discord Bot API Token。 BotToken string `json:"botToken"` // Discord Channel ID。 ChannelId string `json:"channelId"` } type Notifier struct { config *NotifierConfig logger *slog.Logger httpClient *resty.Client } var _ notifier.Provider = (*Notifier)(nil) func NewNotifier(config *NotifierConfig) (*Notifier, error) { if config == nil { return nil, errors.New("the configuration of the notifier provider is nil") } client := resty.New(). SetHeader("Authorization", fmt.Sprintf("Bot %s", config.BotToken)). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent) return &Notifier{ config: config, logger: slog.Default(), httpClient: client, }, nil } func (n *Notifier) SetLogger(logger *slog.Logger) { if logger == nil { n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } } func (n *Notifier) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { // REF: https://discord.com/developers/docs/resources/message#create-message req := n.httpClient.R(). SetContext(ctx). SetBody(map[string]any{ "content": subject + "\n" + message, }) resp, err := req.Post(fmt.Sprintf("https://discord.com/api/v9/channels/%s/messages", n.config.ChannelId)) if err != nil { return nil, fmt.Errorf("discord api error: failed to send request: %w", err) } else if resp.IsError() { return nil, fmt.Errorf("discord api error: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return ¬ifier.NotifyResult{}, nil } ================================================ FILE: pkg/core/notifier/providers/discordbot/discordbot_test.go ================================================ package discordbot_test import ( "context" "flag" "fmt" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/notifier/providers/discordbot" ) const ( mockSubject = "test_subject" mockMessage = "test_message" ) var ( fApiToken string fChannelId string ) func init() { argsPrefix := "DISCORDBOT_" flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "") flag.StringVar(&fChannelId, argsPrefix+"CHANNELID", "", "") } /* Shell command to run this test: go test -v ./discordbot_test.go -args \ --DISCORDBOT_APITOKEN="your-bot-token" \ --DISCORDBOT_CHANNELID="your-channel-id" */ func TestNotify(t *testing.T) { flag.Parse() t.Run("Notify", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("APITOKEN: %v", fApiToken), fmt.Sprintf("CHANNELID: %v", fChannelId), }, "\n")) provider, err := provider.NewNotifier(&provider.NotifierConfig{ BotToken: fApiToken, ChannelId: fChannelId, }) if err != nil { t.Errorf("err: %+v", err) return } res, err := provider.Notify(context.Background(), mockSubject, mockMessage) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/notifier/providers/email/consts.go ================================================ package email const ( MESSAGE_FORMAT_PLAIN = "plain" MESSAGE_FORMAT_HTML = "html" ) ================================================ FILE: pkg/core/notifier/providers/email/email.go ================================================ package email import ( "context" "errors" "fmt" "log/slog" "github.com/microcosm-cc/bluemonday" "github.com/certimate-go/certimate/internal/tools/smtp" "github.com/certimate-go/certimate/pkg/core/notifier" ) type NotifierConfig struct { // SMTP 服务器地址。 SmtpHost string `json:"smtpHost"` // SMTP 服务器端口。 // 零值时根据是否启用 TLS 决定。 SmtpPort int32 `json:"smtpPort"` // 是否启用 TLS。 SmtpTls bool `json:"smtpTls"` // 用户名。 Username string `json:"username"` // 密码。 Password string `json:"password"` // 发件人邮箱。 SenderAddress string `json:"senderAddress"` // 发件人显示名称。 SenderName string `json:"senderName,omitempty"` // 收件人邮箱。 ReceiverAddress string `json:"receiverAddress"` // 消息格式。 // 可取值 [MESSAGE_FORMAT_PLAIN]、[MESSAGE_FORMAT_HTML]。 // 零值时默认值 [MESSAGE_FORMAT_PLAIN]。 MessageFormat string `json:"messageFormat,omitempty"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type Notifier struct { config *NotifierConfig logger *slog.Logger } var _ notifier.Provider = (*Notifier)(nil) func NewNotifier(config *NotifierConfig) (*Notifier, error) { if config == nil { return nil, errors.New("the configuration of the notifier provider is nil") } return &Notifier{ config: config, logger: slog.Default(), }, nil } func (n *Notifier) SetLogger(logger *slog.Logger) { if logger == nil { n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } } func (n *Notifier) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { clientCfg := smtp.NewDefaultConfig() clientCfg.Host = n.config.SmtpHost clientCfg.Port = int(n.config.SmtpPort) clientCfg.Username = n.config.Username clientCfg.Password = n.config.Password clientCfg.UseSsl = n.config.SmtpTls clientCfg.SkipTlsVerify = n.config.AllowInsecureConnections client, err := smtp.NewClient(clientCfg) if err != nil { return nil, fmt.Errorf("failed to create SMTP client: %w", err) } defer client.Close() msg := smtp.NewMessage() msg.Subject(subject) switch n.config.MessageFormat { case "", MESSAGE_FORMAT_PLAIN: msg.SetBodyString(smtp.MIMETypeTextPlain, message) case MESSAGE_FORMAT_HTML: msg.SetBodyString(smtp.MIMETypeTextHTML, bluemonday.UGCPolicy().Sanitize(message)) msg.AddAlternativeString(smtp.MIMETypeTextPlain, bluemonday.StrictPolicy().Sanitize(message)) default: return nil, fmt.Errorf("unsupported message format: '%s'", n.config.MessageFormat) } if n.config.SenderName == "" { msg.From(n.config.SenderAddress) } else { msg.FromFormat(n.config.SenderName, n.config.SenderAddress) } msg.To(n.config.ReceiverAddress) if err := client.Send(ctx, msg); err != nil { return nil, fmt.Errorf("failed to send mail: %w", err) } return ¬ifier.NotifyResult{}, nil } ================================================ FILE: pkg/core/notifier/providers/email/email_test.go ================================================ package email_test import ( "context" "flag" "fmt" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/notifier/providers/email" ) const ( mockSubject = "test_subject" mockMessage = "test_message" mockHtmlMessage = "

Hello Certimate!

Google" ) var ( fSmtpHost string fSmtpPort int64 fSmtpTLS bool fUsername string fPassword string fSenderAddress string fReceiverAddress string ) func init() { argsPrefix := "EMAIL_" flag.StringVar(&fSmtpHost, argsPrefix+"SMTPHOST", "", "") flag.Int64Var(&fSmtpPort, argsPrefix+"SMTPPORT", 0, "") flag.BoolVar(&fSmtpTLS, argsPrefix+"SMTPTLS", false, "") flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "") flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "") flag.StringVar(&fSenderAddress, argsPrefix+"SENDERADDRESS", "", "") flag.StringVar(&fReceiverAddress, argsPrefix+"RECEIVERADDRESS", "", "") } /* Shell command to run this test: go test -v ./email_test.go -args \ --EMAIL_SMTPHOST="smtp.example.com" \ --EMAIL_SMTPPORT=465 \ --EMAIL_SMTPTLS=true \ --EMAIL_USERNAME="your-username" \ --EMAIL_PASSWORD="your-password" \ --EMAIL_SENDERADDRESS="sender@example.com" \ --EMAIL_RECEIVERADDRESS="receiver@example.com" */ func TestNotify(t *testing.T) { flag.Parse() t.Run("Notify", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("SMTPHOST: %v", fSmtpHost), fmt.Sprintf("SMTPPORT: %v", fSmtpPort), fmt.Sprintf("SMTPTLS: %v", fSmtpTLS), fmt.Sprintf("USERNAME: %v", fUsername), fmt.Sprintf("PASSWORD: %v", fPassword), fmt.Sprintf("SENDERADDRESS: %v", fSenderAddress), fmt.Sprintf("RECEIVERADDRESS: %v", fReceiverAddress), }, "\n")) provider, err := provider.NewNotifier(&provider.NotifierConfig{ SmtpHost: fSmtpHost, SmtpPort: int32(fSmtpPort), SmtpTls: fSmtpTLS, Username: fUsername, Password: fPassword, SenderAddress: fSenderAddress, ReceiverAddress: fReceiverAddress, }) if err != nil { t.Errorf("err: %+v", err) return } res, err := provider.Notify(context.Background(), mockSubject, mockMessage) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) t.Run("Notify_Html", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("SMTPHOST: %v", fSmtpHost), fmt.Sprintf("SMTPPORT: %v", fSmtpPort), fmt.Sprintf("SMTPTLS: %v", fSmtpTLS), fmt.Sprintf("USERNAME: %v", fUsername), fmt.Sprintf("PASSWORD: %v", fPassword), fmt.Sprintf("SENDERADDRESS: %v", fSenderAddress), fmt.Sprintf("RECEIVERADDRESS: %v", fReceiverAddress), }, "\n")) provider, err := provider.NewNotifier(&provider.NotifierConfig{ SmtpHost: fSmtpHost, SmtpPort: int32(fSmtpPort), SmtpTls: fSmtpTLS, Username: fUsername, Password: fPassword, SenderAddress: fSenderAddress, ReceiverAddress: fReceiverAddress, MessageFormat: provider.MESSAGE_FORMAT_HTML, }) if err != nil { t.Errorf("err: %+v", err) return } res, err := provider.Notify(context.Background(), mockSubject, mockHtmlMessage) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/notifier/providers/larkbot/larkbot.go ================================================ package larkbot import ( "context" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "errors" "fmt" "log/slog" "net/url" "strings" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/pkg/core/notifier" ) type NotifierConfig struct { // 飞书机器人 Webhook 地址。 WebhookUrl string `json:"webhookUrl"` // 飞书机器人的 Secret。 Secret string `json:"secret"` // 自定义消息数据。 // 选填。 CustomPayload string `json:"customPayload,omitempty"` } type Notifier struct { config *NotifierConfig logger *slog.Logger httpClient *resty.Client } var _ notifier.Provider = (*Notifier)(nil) func NewNotifier(config *NotifierConfig) (*Notifier, error) { if config == nil { return nil, errors.New("the configuration of the notifier provider is nil") } client := resty.New(). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent) return &Notifier{ config: config, logger: slog.Default(), httpClient: client, }, nil } func (n *Notifier) SetLogger(logger *slog.Logger) { if logger == nil { n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } } func (n *Notifier) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { webhookUrl, err := url.Parse(n.config.WebhookUrl) if err != nil { return nil, fmt.Errorf("lark api error: invalid webhook url: %w", err) } else { const hostname = "open.larksuite.com" const hostname_cn = "open.feishu.cn" if webhookUrl.Hostname() != hostname && webhookUrl.Hostname() != hostname_cn { n.logger.Warn(fmt.Sprintf("the webhook url hostname is not '%s' or '%s', please make sure it is correct", hostname, hostname_cn)) } } var webhookData map[string]any if n.config.CustomPayload == "" { webhookData = map[string]any{ "msg_type": "text", "content": map[string]string{ "text": subject + "\n\n" + message, }, } } else { err = json.Unmarshal([]byte(n.config.CustomPayload), &webhookData) if err != nil { return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err) } replaceJsonValueRecursively(webhookData, "${CERTIMATE_NOTIFIER_SUBJECT}", subject) replaceJsonValueRecursively(webhookData, "${CERTIMATE_NOTIFIER_MESSAGE}", message) replaceJsonValueRecursively(webhookData, "${SUBJECT}", subject) replaceJsonValueRecursively(webhookData, "${MESSAGE}", message) } if n.config.Secret != "" { timestamp := fmt.Sprintf("%d", time.Now().Unix()) stringToSign := fmt.Sprintf("%s\n%s", timestamp, n.config.Secret) h := hmac.New(sha256.New, []byte(stringToSign)) var data []byte _, err := h.Write(data) if err != nil { return nil, fmt.Errorf("lark api error: failed to calc sign: %w", err) } sign := base64.StdEncoding.EncodeToString(h.Sum(nil)) webhookData["timestamp"] = timestamp webhookData["sign"] = sign } // REF: https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot // REF: https://open.larksuite.com/document/client-docs/bot-v3/add-custom-bot var result struct { Code int `json:"code"` Message string `json:"msg"` } req := n.httpClient.R(). SetContext(ctx). SetBody(webhookData) resp, err := req.Post(webhookUrl.String()) if err != nil { return nil, fmt.Errorf("lark api error: failed to send request: %w", err) } else if resp.IsError() { return nil, fmt.Errorf("lark api error: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } else if err := json.Unmarshal(resp.Body(), &result); err != nil { return nil, fmt.Errorf("lark api error: %w (resp: %s)", err, resp.String()) } else if result.Code != 0 { return nil, fmt.Errorf("lark api error: code='%d', msg='%s'", result.Code, result.Message) } return ¬ifier.NotifyResult{}, nil } func replaceJsonValueRecursively(data interface{}, oldStr, newStr string) interface{} { switch v := data.(type) { case map[string]any: for k, val := range v { v[k] = replaceJsonValueRecursively(val, oldStr, newStr) } case []any: for i, val := range v { v[i] = replaceJsonValueRecursively(val, oldStr, newStr) } case []string: for i, s := range v { var val interface{} = s var newVal interface{} = replaceJsonValueRecursively(val, oldStr, newStr) v[i] = newVal.(string) } case string: return strings.ReplaceAll(v, oldStr, newStr) } return data } ================================================ FILE: pkg/core/notifier/providers/larkbot/larkbot_test.go ================================================ package larkbot_test import ( "context" "flag" "fmt" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/notifier/providers/larkbot" ) const ( mockSubject = "test_subject" mockMessage = "test_message" ) var ( fWebhookUrl string fSecret string ) func init() { argsPrefix := "LARKBOT_" flag.StringVar(&fWebhookUrl, argsPrefix+"WEBHOOKURL", "", "") flag.StringVar(&fSecret, argsPrefix+"SECRET", "", "") } /* Shell command to run this test: go test -v ./larkbot_test.go -args \ --LARKBOT_WEBHOOKURL="https://example.com/your-webhook-url" \ --LARKBOT_SECRET="your-secret" */ func TestNotify(t *testing.T) { flag.Parse() t.Run("Notify", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("WEBHOOKURL: %v", fWebhookUrl), fmt.Sprintf("SECRET: %v", fSecret), }, "\n")) provider, err := provider.NewNotifier(&provider.NotifierConfig{ WebhookUrl: fWebhookUrl, Secret: fSecret, }) if err != nil { t.Errorf("err: %+v", err) return } res, err := provider.Notify(context.Background(), mockSubject, mockMessage) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/notifier/providers/mattermost/mattermost.go ================================================ package mattermost import ( "context" "errors" "fmt" "log/slog" "strings" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/pkg/core/notifier" ) type NotifierConfig struct { // Mattermost 服务地址。 ServerUrl string `json:"serverUrl"` // Mattermost 用户名。 Username string `json:"username"` // Mattermost 密码。 Password string `json:"password"` // Mattermost 频道 ID。 ChannelId string `json:"channelId"` } type Notifier struct { config *NotifierConfig logger *slog.Logger httpClient *resty.Client } var _ notifier.Provider = (*Notifier)(nil) func NewNotifier(config *NotifierConfig) (*Notifier, error) { if config == nil { return nil, errors.New("the configuration of the notifier provider is nil") } client := resty.New(). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent) return &Notifier{ config: config, logger: slog.Default(), httpClient: client, }, nil } func (n *Notifier) SetLogger(logger *slog.Logger) { if logger == nil { n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } } func (n *Notifier) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { serverUrl := strings.TrimRight(n.config.ServerUrl, "/") // REF: https://developers.mattermost.com/api-documentation/#/operations/Login loginReq := n.httpClient.R(). SetContext(ctx). SetBody(map[string]any{ "login_id": n.config.Username, "password": n.config.Password, }) loginResp, err := loginReq.Post(fmt.Sprintf("%s/api/v4/users/login", serverUrl)) if err != nil { return nil, fmt.Errorf("mattermost api error: failed to send request: %w", err) } else if loginResp.IsError() { return nil, fmt.Errorf("mattermost api error: unexpected status code: %d (resp: %s)", loginResp.StatusCode(), loginResp.String()) } else if loginResp.Header().Get("Token") == "" { return nil, fmt.Errorf("mattermost api error: received empty login token") } // REF: https://developers.mattermost.com/api-documentation/#/operations/CreatePost postReq := n.httpClient.R(). SetContext(ctx). SetHeader("Authorization", "Bearer "+loginResp.Header().Get("Token")). SetBody(map[string]any{ "channel_id": n.config.ChannelId, "props": map[string]interface{}{ "attachments": []map[string]interface{}{ { "title": subject, "text": message, }, }, }, }) postResp, err := postReq.Post(fmt.Sprintf("%s/api/v4/posts", serverUrl)) if err != nil { return nil, fmt.Errorf("mattermost api error: failed to send request: %w", err) } else if postResp.IsError() { return nil, fmt.Errorf("mattermost api error: unexpected status code: %d (resp: %s)", postResp.StatusCode(), postResp.String()) } return ¬ifier.NotifyResult{}, nil } ================================================ FILE: pkg/core/notifier/providers/mattermost/mattermost_test.go ================================================ package mattermost_test import ( "context" "flag" "fmt" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/notifier/providers/mattermost" ) const ( mockSubject = "test_subject" mockMessage = "test_message" ) var ( fServerUrl string fChannelId string fUsername string fPassword string ) func init() { argsPrefix := "MATTERMOST_" flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") flag.StringVar(&fChannelId, argsPrefix+"CHANNELID", "", "") flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "") flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "") } /* Shell command to run this test: go test -v ./mattermost_test.go -args \ --MATTERMOST_SERVERURL="https://example.com/your-server-url" \ --MATTERMOST_CHANNELID="your-chanel-id" \ --MATTERMOST_USERNAME="your-username" \ --MATTERMOST_PASSWORD="your-password" */ func TestNotify(t *testing.T) { flag.Parse() t.Run("Notify", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("SERVERURL: %v", fServerUrl), fmt.Sprintf("CHANNELID: %v", fChannelId), fmt.Sprintf("USERNAME: %v", fUsername), fmt.Sprintf("PASSWORD: %v", fPassword), }, "\n")) provider, err := provider.NewNotifier(&provider.NotifierConfig{ ServerUrl: fServerUrl, ChannelId: fChannelId, Username: fUsername, Password: fPassword, }) if err != nil { t.Errorf("err: %+v", err) return } res, err := provider.Notify(context.Background(), mockSubject, mockMessage) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/notifier/providers/slackbot/slackbot.go ================================================ package discordbot import ( "context" "errors" "fmt" "log/slog" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/pkg/core/notifier" ) type NotifierConfig struct { // Slack Bot API Token。 BotToken string `json:"botToken"` // Slack Channel ID。 ChannelId string `json:"channelId"` } type Notifier struct { config *NotifierConfig logger *slog.Logger httpClient *resty.Client } var _ notifier.Provider = (*Notifier)(nil) func NewNotifier(config *NotifierConfig) (*Notifier, error) { if config == nil { return nil, errors.New("the configuration of the notifier provider is nil") } client := resty.New(). SetHeader("Authorization", fmt.Sprintf("Bearer %s", config.BotToken)). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent) return &Notifier{ config: config, logger: slog.Default(), httpClient: client, }, nil } func (n *Notifier) SetLogger(logger *slog.Logger) { if logger == nil { n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } } func (n *Notifier) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { // REF: https://docs.slack.dev/messaging/sending-and-scheduling-messages#publishing req := n.httpClient.R(). SetContext(ctx). SetBody(map[string]any{ "token": n.config.BotToken, "channel": n.config.ChannelId, "text": subject + "\n" + message, }) resp, err := req.Post("https://slack.com/api/chat.postMessage") if err != nil { return nil, fmt.Errorf("slack api error: failed to send request: %w", err) } else if resp.IsError() { return nil, fmt.Errorf("slack api error: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return ¬ifier.NotifyResult{}, nil } ================================================ FILE: pkg/core/notifier/providers/slackbot/slackbot_test.go ================================================ package discordbot_test import ( "context" "flag" "fmt" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/notifier/providers/slackbot" ) const ( mockSubject = "test_subject" mockMessage = "test_message" ) var ( fApiToken string fChannelId string ) func init() { argsPrefix := "SLACKBOT_" flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "") flag.StringVar(&fChannelId, argsPrefix+"CHANNELID", "", "") } /* Shell command to run this test: go test -v ./slackbot_test.go -args \ --SLACKBOT_APITOKEN="your-bot-token" \ --SLACKBOT_CHANNELID="your-channel-id" */ func TestNotify(t *testing.T) { flag.Parse() t.Run("Notify", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("APITOKEN: %v", fApiToken), fmt.Sprintf("CHANNELID: %v", fChannelId), }, "\n")) provider, err := provider.NewNotifier(&provider.NotifierConfig{ BotToken: fApiToken, ChannelId: fChannelId, }) if err != nil { t.Errorf("err: %+v", err) return } res, err := provider.Notify(context.Background(), mockSubject, mockMessage) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/notifier/providers/telegrambot/telegrambot.go ================================================ package telegrambot import ( "context" "errors" "fmt" "log/slog" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/pkg/core/notifier" ) type NotifierConfig struct { // Telegram Bot API Token。 BotToken string `json:"botToken"` // Telegram Chat ID。 ChatId string `json:"chatId"` } type Notifier struct { config *NotifierConfig logger *slog.Logger httpClient *resty.Client } var _ notifier.Provider = (*Notifier)(nil) func NewNotifier(config *NotifierConfig) (*Notifier, error) { if config == nil { return nil, errors.New("the configuration of the notifier provider is nil") } client := resty.New(). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent) return &Notifier{ config: config, logger: slog.Default(), httpClient: client, }, nil } func (n *Notifier) SetLogger(logger *slog.Logger) { if logger == nil { n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } } func (n *Notifier) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { // REF: https://core.telegram.org/bots/api#sendmessage req := n.httpClient.R(). SetContext(ctx). SetBody(map[string]any{ "chat_id": n.config.ChatId, "text": subject + "\n" + message, }) resp, err := req.Post(fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", n.config.BotToken)) if err != nil { return nil, fmt.Errorf("telegram api error: failed to send request: %w", err) } else if resp.IsError() { return nil, fmt.Errorf("telegram api error: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return ¬ifier.NotifyResult{}, nil } ================================================ FILE: pkg/core/notifier/providers/telegrambot/telegrambot_test.go ================================================ package telegrambot_test import ( "context" "flag" "fmt" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/notifier/providers/telegrambot" ) const ( mockSubject = "test_subject" mockMessage = "test_message" ) var ( fApiToken string fChatId string ) func init() { argsPrefix := "TELEGRAMBOT_" flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "") flag.StringVar(&fChatId, argsPrefix+"CHATID", "", "") } /* Shell command to run this test: go test -v ./telegrambot_test.go -args \ --TELEGRAMBOT_APITOKEN="your-api-token" \ --TELEGRAMBOT_CHATID="your-chat-id" */ func TestNotify(t *testing.T) { flag.Parse() t.Run("Notify", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("APITOKEN: %v", fApiToken), fmt.Sprintf("CHATID: %v", fChatId), }, "\n")) provider, err := provider.NewNotifier(&provider.NotifierConfig{ BotToken: fApiToken, ChatId: fChatId, }) if err != nil { t.Errorf("err: %+v", err) return } res, err := provider.Notify(context.Background(), mockSubject, mockMessage) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/notifier/providers/webhook/webhook.go ================================================ package webhook import ( "context" "crypto/tls" "encoding/json" "errors" "fmt" "log/slog" "net/http" "net/url" "strings" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/pkg/core/notifier" ) type NotifierConfig struct { // Webhook URL。 WebhookUrl string `json:"webhookUrl"` // Webhook 回调数据(application/json 或 application/x-www-form-urlencoded 格式)。 WebhookData string `json:"webhookData,omitempty"` // 请求谓词。 // 零值时默认值 "POST"。 Method string `json:"method,omitempty"` // 请求标头。 Headers map[string]string `json:"headers,omitempty"` // 请求超时(单位:秒)。 // 零值时默认值 30。 Timeout int `json:"timeout,omitempty"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } type Notifier struct { config *NotifierConfig logger *slog.Logger httpClient *resty.Client } var _ notifier.Provider = (*Notifier)(nil) func NewNotifier(config *NotifierConfig) (*Notifier, error) { if config == nil { return nil, errors.New("the configuration of the notifier provider is nil") } client := resty.New(). SetTimeout(30 * time.Second). SetRetryCount(3). SetRetryWaitTime(5 * time.Second) if config.Timeout > 0 { client.SetTimeout(time.Duration(config.Timeout) * time.Second) } if config.AllowInsecureConnections { client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) } return &Notifier{ config: config, logger: slog.Default(), httpClient: client, }, nil } func (n *Notifier) SetLogger(logger *slog.Logger) { if logger == nil { n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } } func (n *Notifier) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { // 处理 Webhook URL webhookUrl, err := url.Parse(n.config.WebhookUrl) if err != nil { return nil, fmt.Errorf("failed to parse webhook url: %w", err) } else if webhookUrl.Scheme != "http" && webhookUrl.Scheme != "https" { return nil, fmt.Errorf("unsupported webhook url scheme '%s'", webhookUrl.Scheme) } // 处理 Webhook 请求谓词 webhookMethod := strings.ToUpper(n.config.Method) if webhookMethod == "" { webhookMethod = http.MethodPost } else if webhookMethod != http.MethodGet && webhookMethod != http.MethodPost && webhookMethod != http.MethodPut && webhookMethod != http.MethodPatch && webhookMethod != http.MethodDelete { return nil, fmt.Errorf("unsupported webhook request method '%s'", webhookMethod) } // 处理 Webhook 请求标头 webhookHeaders := make(http.Header) for k, v := range n.config.Headers { webhookHeaders.Set(k, v) } // 处理 Webhook 请求内容类型 const CONTENT_TYPE_JSON = "application/json" const CONTENT_TYPE_FORM = "application/x-www-form-urlencoded" const CONTENT_TYPE_MULTIPART = "multipart/form-data" webhookContentType := webhookHeaders.Get("Content-Type") if webhookContentType == "" { webhookContentType = CONTENT_TYPE_JSON webhookHeaders.Set("Content-Type", CONTENT_TYPE_JSON) } else if strings.HasPrefix(webhookContentType, CONTENT_TYPE_JSON) && strings.HasPrefix(webhookContentType, CONTENT_TYPE_FORM) && strings.HasPrefix(webhookContentType, CONTENT_TYPE_MULTIPART) { return nil, fmt.Errorf("unsupported webhook content type '%s'", webhookContentType) } // 处理 Webhook 请求数据 var webhookData interface{} if n.config.WebhookData == "" { webhookData = map[string]string{ "subject": subject, "message": message, } } else { err = json.Unmarshal([]byte(n.config.WebhookData), &webhookData) if err != nil { return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err) } if webhookMethod == http.MethodGet || webhookContentType == CONTENT_TYPE_FORM || webhookContentType == CONTENT_TYPE_MULTIPART { temp := make(map[string]string) jsonb, err := json.Marshal(webhookData) if err != nil { return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err) } else if err := json.Unmarshal(jsonb, &temp); err != nil { return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err) } else { webhookData = temp } } } // 替换变量值 replaceJsonValueRecursively(webhookData, "${CERTIMATE_NOTIFIER_SUBJECT}", subject) replaceJsonValueRecursively(webhookData, "${CERTIMATE_NOTIFIER_MESSAGE}", message) // 兼容旧版变量 replaceJsonValueRecursively(webhookData, "${SUBJECT}", subject) replaceJsonValueRecursively(webhookData, "${MESSAGE}", message) // 生成请求 // 其中 GET 请求需转换为查询参数 req := n.httpClient.R().SetContext(ctx).SetHeaderMultiValues(webhookHeaders) req.URL = webhookUrl.String() req.Method = webhookMethod if webhookMethod == http.MethodGet { req.SetQueryParams(webhookData.(map[string]string)) } else { switch webhookContentType { case CONTENT_TYPE_JSON: req.SetBody(webhookData) case CONTENT_TYPE_FORM: req.SetFormData(webhookData.(map[string]string)) case CONTENT_TYPE_MULTIPART: req.SetMultipartFormData(webhookData.(map[string]string)) } } // 发送请求 resp, err := req.Send() if err != nil { return nil, fmt.Errorf("webhook error: failed to send request: %w", err) } else if resp.IsError() { return nil, fmt.Errorf("webhook error: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } n.logger.Debug("webhook responded", slog.String("response", resp.String())) return ¬ifier.NotifyResult{}, nil } func replaceJsonValueRecursively(data interface{}, oldStr, newStr string) interface{} { switch v := data.(type) { case map[string]any: for k, val := range v { v[k] = replaceJsonValueRecursively(val, oldStr, newStr) } case []any: for i, val := range v { v[i] = replaceJsonValueRecursively(val, oldStr, newStr) } case []string: for i, s := range v { var val interface{} = s var newVal interface{} = replaceJsonValueRecursively(val, oldStr, newStr) v[i] = newVal.(string) } case string: return strings.ReplaceAll(v, oldStr, newStr) } return data } ================================================ FILE: pkg/core/notifier/providers/webhook/webhook_test.go ================================================ package webhook_test import ( "context" "flag" "fmt" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/notifier/providers/webhook" ) const ( mockSubject = "test_subject" mockMessage = "test_message" ) var ( fWebhookUrl string fWebhookContentType string ) func init() { argsPrefix := "WEBHOOK_" flag.StringVar(&fWebhookUrl, argsPrefix+"URL", "", "") flag.StringVar(&fWebhookContentType, argsPrefix+"CONTENTTYPE", "application/json", "") } /* Shell command to run this test: go test -v ./webhook_test.go -args \ --WEBHOOK_URL="https://example.com/your-webhook-url" \ --WEBHOOK_CONTENTTYPE="application/json" */ func TestNotify(t *testing.T) { flag.Parse() t.Run("Notify", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("URL: %v", fWebhookUrl), }, "\n")) provider, err := provider.NewNotifier(&provider.NotifierConfig{ WebhookUrl: fWebhookUrl, Method: "POST", Headers: map[string]string{ "Content-Type": fWebhookContentType, }, AllowInsecureConnections: true, }) if err != nil { t.Errorf("err: %+v", err) return } res, err := provider.Notify(context.Background(), mockSubject, mockMessage) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/notifier/providers/wecombot/wecombot.go ================================================ package wecombot import ( "context" "encoding/json" "errors" "fmt" "log/slog" "net/url" "strings" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" "github.com/certimate-go/certimate/pkg/core/notifier" ) type NotifierConfig struct { // 企业微信机器人 Webhook 地址。 WebhookUrl string `json:"webhookUrl"` // 自定义消息数据。 // 选填。 CustomPayload string `json:"customPayload,omitempty"` } type Notifier struct { config *NotifierConfig logger *slog.Logger httpClient *resty.Client } var _ notifier.Provider = (*Notifier)(nil) func NewNotifier(config *NotifierConfig) (*Notifier, error) { if config == nil { return nil, errors.New("the configuration of the notifier provider is nil") } client := resty.New(). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent) return &Notifier{ config: config, logger: slog.Default(), httpClient: client, }, nil } func (n *Notifier) SetLogger(logger *slog.Logger) { if logger == nil { n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } } func (n *Notifier) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { webhookUrl, err := url.Parse(n.config.WebhookUrl) if err != nil { return nil, fmt.Errorf("dingtalk api error: invalid webhook url: %w", err) } else { const hostname = "qyapi.weixin.qq.com" if webhookUrl.Hostname() != hostname { n.logger.Warn(fmt.Sprintf("the webhook url hostname is not '%s', please make sure it is correct", hostname)) } } var webhookData map[string]any if n.config.CustomPayload == "" { webhookData = map[string]any{ "msgtype": "text", "text": map[string]string{ "content": subject + "\n\n" + message, }, } } else { err = json.Unmarshal([]byte(n.config.CustomPayload), &webhookData) if err != nil { return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err) } replaceJsonValueRecursively(webhookData, "${CERTIMATE_NOTIFIER_SUBJECT}", subject) replaceJsonValueRecursively(webhookData, "${CERTIMATE_NOTIFIER_MESSAGE}", message) replaceJsonValueRecursively(webhookData, "${SUBJECT}", subject) replaceJsonValueRecursively(webhookData, "${MESSAGE}", message) } // REF: https://developer.work.weixin.qq.com/document/path/91770 var result struct { ErrorCode int `json:"errcode"` ErrorMessage string `json:"errmsg"` } req := n.httpClient.R(). SetContext(ctx). SetBody(webhookData) resp, err := req.Post(webhookUrl.String()) if err != nil { return nil, fmt.Errorf("wecom api error: failed to send request: %w", err) } else if resp.IsError() { return nil, fmt.Errorf("wecom api error: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } else if err := json.Unmarshal(resp.Body(), &result); err != nil { return nil, fmt.Errorf("wecom api error: %w (resp: %s)", err, string(resp.Body())) } else if result.ErrorCode != 0 { return nil, fmt.Errorf("wecom api error: errcode='%d', errmsg='%s'", result.ErrorCode, result.ErrorMessage) } return ¬ifier.NotifyResult{}, nil } func replaceJsonValueRecursively(data interface{}, oldStr, newStr string) interface{} { switch v := data.(type) { case map[string]any: for k, val := range v { v[k] = replaceJsonValueRecursively(val, oldStr, newStr) } case []any: for i, val := range v { v[i] = replaceJsonValueRecursively(val, oldStr, newStr) } case []string: for i, s := range v { var val interface{} = s var newVal interface{} = replaceJsonValueRecursively(val, oldStr, newStr) v[i] = newVal.(string) } case string: return strings.ReplaceAll(v, oldStr, newStr) } return data } ================================================ FILE: pkg/core/notifier/providers/wecombot/wecombot_test.go ================================================ package wecombot_test import ( "context" "flag" "fmt" "strings" "testing" provider "github.com/certimate-go/certimate/pkg/core/notifier/providers/wecombot" ) const ( mockSubject = "test_subject" mockMessage = "test_message" ) var fWebhookUrl string func init() { argsPrefix := "WECOMBOT_" flag.StringVar(&fWebhookUrl, argsPrefix+"WEBHOOKURL", "", "") } /* Shell command to run this test: go test -v ./wecombot_test.go -args \ --WECOMBOT_WEBHOOKURL="https://example.com/your-webhook-url" \ */ func TestNotify(t *testing.T) { flag.Parse() t.Run("Notify", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("WEBHOOKURL: %v", fWebhookUrl), }, "\n")) provider, err := provider.NewNotifier(&provider.NotifierConfig{ WebhookUrl: fWebhookUrl, }) if err != nil { t.Errorf("err: %+v", err) return } res, err := provider.Notify(context.Background(), mockSubject, mockMessage) if err != nil { t.Errorf("err: %+v", err) return } t.Logf("ok: %v", res) }) } ================================================ FILE: pkg/core/shared.go ================================================ package core import ( "log/slog" ) type WithLogger interface { SetLogger(logger *slog.Logger) } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/README.md ================================================ 移动云 Go SDK 文档: [https://ecloud.10086.cn/op-help-center/doc/article/53799](https://ecloud.10086.cn/op-help-center/doc/article/53799) 移动云 Go SDK 下载地址: [https://ecloud.10086.cn/api/query/developer/nexus/service/rest/repository/browse/go-sdk/gitlab.ecloud.com/ecloud/](https://ecloud.10086.cn/api/query/developer/nexus/service/rest/repository/browse/go-sdk/gitlab.ecloud.com/ecloud/) --- 将其引入本地目录的原因是: 1. 原始包必须通过移动云私有仓库获取, 为构建带来不便。 2. 原始包存在部分内容错误, 需要自行修改, 如: - 存在一些编译错误; - 返回错误的时候, 未返回错误信息; - 解析响应体错误。 ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/client.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package ecloudsdkclouddns import ( "gitlab.ecloud.com/ecloud/ecloudsdkclouddns/model" "gitlab.ecloud.com/ecloud/ecloudsdkcore" "gitlab.ecloud.com/ecloud/ecloudsdkcore/config" ) type Client struct { APIClient *ecloudsdkcore.APIClient config *config.Config httpRequest *ecloudsdkcore.HttpRequest } func NewClient(config *config.Config) *Client { client := &Client{} client.config = config apiClient := ecloudsdkcore.NewAPIClient() httpRequest := ecloudsdkcore.NewDefaultHttpRequest() httpRequest.Product = product httpRequest.Version = version httpRequest.SdkVersion = sdkVersion client.httpRequest = httpRequest client.APIClient = apiClient return client } func NewClientByCustomized(config *config.Config, httpRequest *ecloudsdkcore.HttpRequest) *Client { client := &Client{} client.config = config apiClient := ecloudsdkcore.NewAPIClient() httpRequest.Product = product httpRequest.Version = version httpRequest.SdkVersion = sdkVersion client.httpRequest = httpRequest client.APIClient = apiClient return client } const ( product string = "clouddns" version string = "v1" sdkVersion string = "1.0.1" ) // CreateRecord 新增解析记录 func (c *Client) CreateRecord(request *model.CreateRecordRequest) (*model.CreateRecordResponse, error) { c.httpRequest.Action = "createRecord" c.httpRequest.Body = request returnValue := &model.CreateRecordResponse{} if _, err := c.APIClient.Excute(c.httpRequest, c.config, returnValue); err != nil { return nil, err } else { return returnValue, nil } } // CreateRecordOpenapi 新增解析记录Openapi func (c *Client) CreateRecordOpenapi(request *model.CreateRecordOpenapiRequest) (*model.CreateRecordOpenapiResponse, error) { c.httpRequest.Action = "createRecordOpenapi" c.httpRequest.Body = request returnValue := &model.CreateRecordOpenapiResponse{} if _, err := c.APIClient.Excute(c.httpRequest, c.config, returnValue); err != nil { return nil, err } else { return returnValue, nil } } // DeleteRecord 删除解析记录 func (c *Client) DeleteRecord(request *model.DeleteRecordRequest) (*model.DeleteRecordResponse, error) { c.httpRequest.Action = "deleteRecord" c.httpRequest.Body = request returnValue := &model.DeleteRecordResponse{} if _, err := c.APIClient.Excute(c.httpRequest, c.config, returnValue); err != nil { return nil, err } else { return returnValue, nil } } // DeleteRecordOpenapi 删除解析记录Openapi func (c *Client) DeleteRecordOpenapi(request *model.DeleteRecordOpenapiRequest) (*model.DeleteRecordOpenapiResponse, error) { c.httpRequest.Action = "deleteRecordOpenapi" c.httpRequest.Body = request returnValue := &model.DeleteRecordOpenapiResponse{} if _, err := c.APIClient.Excute(c.httpRequest, c.config, returnValue); err != nil { return nil, err } else { return returnValue, nil } } // ListRecord 查询解析记录 func (c *Client) ListRecord(request *model.ListRecordRequest) (*model.ListRecordResponse, error) { c.httpRequest.Action = "listRecord" c.httpRequest.Body = request returnValue := &model.ListRecordResponse{} if _, err := c.APIClient.Excute(c.httpRequest, c.config, returnValue); err != nil { return nil, err } else { return returnValue, nil } } // ListRecordOpenapi 查询解析记录Openapi func (c *Client) ListRecordOpenapi(request *model.ListRecordOpenapiRequest) (*model.ListRecordOpenapiResponse, error) { c.httpRequest.Action = "listRecordOpenapi" c.httpRequest.Body = request returnValue := &model.ListRecordOpenapiResponse{} if _, err := c.APIClient.Excute(c.httpRequest, c.config, returnValue); err != nil { return nil, err } else { return returnValue, nil } } // ModifyRecord 修改解析记录 func (c *Client) ModifyRecord(request *model.ModifyRecordRequest) (*model.ModifyRecordResponse, error) { c.httpRequest.Action = "modifyRecord" c.httpRequest.Body = request returnValue := &model.ModifyRecordResponse{} if _, err := c.APIClient.Excute(c.httpRequest, c.config, returnValue); err != nil { return nil, err } else { return returnValue, nil } } // ModifyRecordOpenapi 修改解析记录Openapi func (c *Client) ModifyRecordOpenapi(request *model.ModifyRecordOpenapiRequest) (*model.ModifyRecordOpenapiResponse, error) { c.httpRequest.Action = "modifyRecordOpenapi" c.httpRequest.Body = request returnValue := &model.ModifyRecordOpenapiResponse{} if _, err := c.APIClient.Excute(c.httpRequest, c.config, returnValue); err != nil { return nil, err } else { return returnValue, nil } } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/go.mod ================================================ module gitlab.ecloud.com/ecloud/ecloudsdkclouddns go 1.14 require gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0 replace gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0 => ../ecloudsdkcore@v1.0.0 ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/create_record_body.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model import ( "gitlab.ecloud.com/ecloud/ecloudsdkcore/position" ) type CreateRecordBodyTypeEnum string // List of Type const ( CreateRecordBodyTypeEnumA CreateRecordBodyTypeEnum = "A" CreateRecordBodyTypeEnumAaaa CreateRecordBodyTypeEnum = "AAAA" CreateRecordBodyTypeEnumCaa CreateRecordBodyTypeEnum = "CAA" CreateRecordBodyTypeEnumCmauth CreateRecordBodyTypeEnum = "CMAUTH" CreateRecordBodyTypeEnumCname CreateRecordBodyTypeEnum = "CNAME" CreateRecordBodyTypeEnumMx CreateRecordBodyTypeEnum = "MX" CreateRecordBodyTypeEnumNs CreateRecordBodyTypeEnum = "NS" CreateRecordBodyTypeEnumPtr CreateRecordBodyTypeEnum = "PTR" CreateRecordBodyTypeEnumRp CreateRecordBodyTypeEnum = "RP" CreateRecordBodyTypeEnumSpf CreateRecordBodyTypeEnum = "SPF" CreateRecordBodyTypeEnumSrv CreateRecordBodyTypeEnum = "SRV" CreateRecordBodyTypeEnumTxt CreateRecordBodyTypeEnum = "TXT" CreateRecordBodyTypeEnumUrl CreateRecordBodyTypeEnum = "URL" ) type CreateRecordBody struct { position.Body // 主机头 Rr string `json:"rr"` // 域名名称 DomainName string `json:"domainName"` // 备注 Description string `json:"description,omitempty"` // 线路ID LineId string `json:"lineId"` // MX优先级,若“记录类型”选择”MX”,则需要配置该参数,默认是5 MxPri *int32 `json:"mxPri,omitempty"` // 记录类型 Type CreateRecordBodyTypeEnum `json:"type"` // 缓存的生命周期,默认可配置600s Ttl *int32 `json:"ttl,omitempty"` // 记录值 Value string `json:"value"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/create_record_openapi_body.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model import ( "gitlab.ecloud.com/ecloud/ecloudsdkcore/position" ) type CreateRecordOpenapiBodyTypeEnum string // List of Type const ( CreateRecordOpenapiBodyTypeEnumA CreateRecordOpenapiBodyTypeEnum = "A" CreateRecordOpenapiBodyTypeEnumAaaa CreateRecordOpenapiBodyTypeEnum = "AAAA" CreateRecordOpenapiBodyTypeEnumCname CreateRecordOpenapiBodyTypeEnum = "CNAME" CreateRecordOpenapiBodyTypeEnumMx CreateRecordOpenapiBodyTypeEnum = "MX" CreateRecordOpenapiBodyTypeEnumTxt CreateRecordOpenapiBodyTypeEnum = "TXT" CreateRecordOpenapiBodyTypeEnumNs CreateRecordOpenapiBodyTypeEnum = "NS" CreateRecordOpenapiBodyTypeEnumSpf CreateRecordOpenapiBodyTypeEnum = "SPF" CreateRecordOpenapiBodyTypeEnumSrv CreateRecordOpenapiBodyTypeEnum = "SRV" CreateRecordOpenapiBodyTypeEnumCaa CreateRecordOpenapiBodyTypeEnum = "CAA" CreateRecordOpenapiBodyTypeEnumCmauth CreateRecordOpenapiBodyTypeEnum = "CMAUTH" ) type CreateRecordOpenapiBody struct { position.Body // 主机头 Rr string `json:"rr"` // 域名名称 DomainName string `json:"domainName"` // 备注 Description string `json:"description,omitempty"` // 线路ID LineId string `json:"lineId"` // MX优先级,若“记录类型”选择”MX”,则需要配置该参数,默认是5 MxPri *int32 `json:"mxPri,omitempty"` // 记录类型 Type CreateRecordOpenapiBodyTypeEnum `json:"type"` // 缓存的生命周期,默认可配置600s Ttl *int32 `json:"ttl,omitempty"` // 记录值 Value string `json:"value"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/create_record_openapi_request.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type CreateRecordOpenapiRequest struct { CreateRecordOpenapiBody *CreateRecordOpenapiBody `json:"createRecordOpenapiBody,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/create_record_openapi_response.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type CreateRecordOpenapiResponseStateEnum string // List of State const ( CreateRecordOpenapiResponseStateEnumError CreateRecordOpenapiResponseStateEnum = "ERROR" CreateRecordOpenapiResponseStateEnumException CreateRecordOpenapiResponseStateEnum = "EXCEPTION" CreateRecordOpenapiResponseStateEnumForbidden CreateRecordOpenapiResponseStateEnum = "FORBIDDEN" CreateRecordOpenapiResponseStateEnumOk CreateRecordOpenapiResponseStateEnum = "OK" ) type CreateRecordOpenapiResponse struct { RequestId string `json:"requestId,omitempty"` ErrorMessage string `json:"errorMessage,omitempty"` ErrorCode string `json:"errorCode,omitempty"` State CreateRecordOpenapiResponseStateEnum `json:"state,omitempty"` Body *CreateRecordOpenapiResponseBody `json:"body,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/create_record_openapi_response_body.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type CreateRecordOpenapiResponseBodyTypeEnum string // List of Type const ( CreateRecordOpenapiResponseBodyTypeEnumA CreateRecordOpenapiResponseBodyTypeEnum = "A" CreateRecordOpenapiResponseBodyTypeEnumAaaa CreateRecordOpenapiResponseBodyTypeEnum = "AAAA" CreateRecordOpenapiResponseBodyTypeEnumCname CreateRecordOpenapiResponseBodyTypeEnum = "CNAME" CreateRecordOpenapiResponseBodyTypeEnumMx CreateRecordOpenapiResponseBodyTypeEnum = "MX" CreateRecordOpenapiResponseBodyTypeEnumTxt CreateRecordOpenapiResponseBodyTypeEnum = "TXT" CreateRecordOpenapiResponseBodyTypeEnumNs CreateRecordOpenapiResponseBodyTypeEnum = "NS" CreateRecordOpenapiResponseBodyTypeEnumSpf CreateRecordOpenapiResponseBodyTypeEnum = "SPF" CreateRecordOpenapiResponseBodyTypeEnumSrv CreateRecordOpenapiResponseBodyTypeEnum = "SRV" CreateRecordOpenapiResponseBodyTypeEnumCaa CreateRecordOpenapiResponseBodyTypeEnum = "CAA" CreateRecordOpenapiResponseBodyTypeEnumCmauth CreateRecordOpenapiResponseBodyTypeEnum = "CMAUTH" ) type CreateRecordOpenapiResponseBodyStateEnum string // List of State const ( CreateRecordOpenapiResponseBodyStateEnumDisabled CreateRecordOpenapiResponseBodyStateEnum = "DISABLED" CreateRecordOpenapiResponseBodyStateEnumEnabled CreateRecordOpenapiResponseBodyStateEnum = "ENABLED" ) type CreateRecordOpenapiResponseBody struct { // 主机头 Rr string `json:"rr,omitempty"` // 修改时间 ModifiedTime string `json:"modifiedTime,omitempty"` // 线路中文名 LineZh string `json:"lineZh,omitempty"` // 备注 Description string `json:"description,omitempty"` // 线路ID LineId string `json:"lineId,omitempty"` // 权重值 Weight *int32 `json:"weight,omitempty"` // MX优先级 MxPri *int32 `json:"mxPri,omitempty"` // 记录类型 Type CreateRecordOpenapiResponseBodyTypeEnum `json:"type,omitempty"` // 缓存的生命周期 Ttl *int32 `json:"ttl,omitempty"` // 标签 Tags *[]CreateRecordOpenapiResponseTags `json:"tags,omitempty"` // 解析记录ID RecordId string `json:"recordId,omitempty"` // 域名名称 DomainName string `json:"domainName,omitempty"` // 线路英文名 LineEn string `json:"lineEn,omitempty"` // 状态 State CreateRecordOpenapiResponseBodyStateEnum `json:"state,omitempty"` // 记录值 Value string `json:"value,omitempty"` // 定时发布时间 Pubdate string `json:"pubdate,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/create_record_openapi_response_tags.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type CreateRecordOpenapiResponseTags struct { // 标签ID TagId string `json:"tagId,omitempty"` // 标签名称 Value string `json:"value,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/create_record_request.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type CreateRecordRequest struct { CreateRecordBody *CreateRecordBody `json:"createRecordBody,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/create_record_response.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type CreateRecordResponseStateEnum string // List of State const ( CreateRecordResponseStateEnumError CreateRecordResponseStateEnum = "ERROR" CreateRecordResponseStateEnumException CreateRecordResponseStateEnum = "EXCEPTION" CreateRecordResponseStateEnumForbidden CreateRecordResponseStateEnum = "FORBIDDEN" CreateRecordResponseStateEnumOk CreateRecordResponseStateEnum = "OK" ) type CreateRecordResponse struct { RequestId string `json:"requestId,omitempty"` ErrorMessage string `json:"errorMessage,omitempty"` ErrorCode string `json:"errorCode,omitempty"` State CreateRecordResponseStateEnum `json:"state,omitempty"` Body *CreateRecordResponseBody `json:"body,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/create_record_response_body.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type CreateRecordResponseBodyTypeEnum string // List of Type const ( CreateRecordResponseBodyTypeEnumA CreateRecordResponseBodyTypeEnum = "A" CreateRecordResponseBodyTypeEnumAaaa CreateRecordResponseBodyTypeEnum = "AAAA" CreateRecordResponseBodyTypeEnumCaa CreateRecordResponseBodyTypeEnum = "CAA" CreateRecordResponseBodyTypeEnumCmauth CreateRecordResponseBodyTypeEnum = "CMAUTH" CreateRecordResponseBodyTypeEnumCname CreateRecordResponseBodyTypeEnum = "CNAME" CreateRecordResponseBodyTypeEnumMx CreateRecordResponseBodyTypeEnum = "MX" CreateRecordResponseBodyTypeEnumNs CreateRecordResponseBodyTypeEnum = "NS" CreateRecordResponseBodyTypeEnumPtr CreateRecordResponseBodyTypeEnum = "PTR" CreateRecordResponseBodyTypeEnumRp CreateRecordResponseBodyTypeEnum = "RP" CreateRecordResponseBodyTypeEnumSpf CreateRecordResponseBodyTypeEnum = "SPF" CreateRecordResponseBodyTypeEnumSrv CreateRecordResponseBodyTypeEnum = "SRV" CreateRecordResponseBodyTypeEnumTxt CreateRecordResponseBodyTypeEnum = "TXT" CreateRecordResponseBodyTypeEnumUrl CreateRecordResponseBodyTypeEnum = "URL" ) type CreateRecordResponseBodyTimedStatusEnum string // List of TimedStatus const ( CreateRecordResponseBodyTimedStatusEnumDisabled CreateRecordResponseBodyTimedStatusEnum = "DISABLED" CreateRecordResponseBodyTimedStatusEnumEnabled CreateRecordResponseBodyTimedStatusEnum = "ENABLED" CreateRecordResponseBodyTimedStatusEnumTimed CreateRecordResponseBodyTimedStatusEnum = "TIMED" ) type CreateRecordResponseBodyStateEnum string // List of State const ( CreateRecordResponseBodyStateEnumDisabled CreateRecordResponseBodyStateEnum = "DISABLED" CreateRecordResponseBodyStateEnumEnabled CreateRecordResponseBodyStateEnum = "ENABLED" ) type CreateRecordResponseBody struct { // 主机头 Rr string `json:"rr,omitempty"` // 修改时间 ModifiedTime string `json:"modifiedTime,omitempty"` // 线路中文名 LineZh string `json:"lineZh,omitempty"` // 备注 Description string `json:"description,omitempty"` // 线路ID LineId string `json:"lineId,omitempty"` // 权重值 Weight *int32 `json:"weight,omitempty"` // MX优先级 MxPri *int32 `json:"mxPri,omitempty"` // 记录类型 Type CreateRecordResponseBodyTypeEnum `json:"type,omitempty"` // 缓存的生命周期 Ttl *int32 `json:"ttl,omitempty"` // 标签 Tags *[]CreateRecordResponseTags `json:"tags,omitempty"` // 解析记录ID RecordId string `json:"recordId,omitempty"` // 定时状态 TimedStatus CreateRecordResponseBodyTimedStatusEnum `json:"timedStatus,omitempty"` // 域名名称 DomainName string `json:"domainName,omitempty"` // 线路英文名 LineEn string `json:"lineEn,omitempty"` // 状态 State CreateRecordResponseBodyStateEnum `json:"state,omitempty"` // 记录值 Value string `json:"value,omitempty"` // 定时发布时间 Pubdate string `json:"pubdate,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/create_record_response_tags.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type CreateRecordResponseTags struct { // 标签ID TagId string `json:"tagId,omitempty"` // 标签名称 Value string `json:"value,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/delete_record_body.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model import ( "gitlab.ecloud.com/ecloud/ecloudsdkcore/position" ) type DeleteRecordBody struct { position.Body // 解析记录ID列表 RecordIdList []string `json:"recordIdList"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/delete_record_openapi_body.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model import ( "gitlab.ecloud.com/ecloud/ecloudsdkcore/position" ) type DeleteRecordOpenapiBody struct { position.Body // 待删除的解析记录ID请求体 RecordIdList []string `json:"recordIdList"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/delete_record_openapi_request.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type DeleteRecordOpenapiRequest struct { DeleteRecordOpenapiBody *DeleteRecordOpenapiBody `json:"deleteRecordOpenapiBody,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/delete_record_openapi_response.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type DeleteRecordOpenapiResponseStateEnum string // List of State const ( DeleteRecordOpenapiResponseStateEnumError DeleteRecordOpenapiResponseStateEnum = "ERROR" DeleteRecordOpenapiResponseStateEnumException DeleteRecordOpenapiResponseStateEnum = "EXCEPTION" DeleteRecordOpenapiResponseStateEnumForbidden DeleteRecordOpenapiResponseStateEnum = "FORBIDDEN" DeleteRecordOpenapiResponseStateEnumOk DeleteRecordOpenapiResponseStateEnum = "OK" ) type DeleteRecordOpenapiResponse struct { RequestId string `json:"requestId,omitempty"` ErrorMessage string `json:"errorMessage,omitempty"` ErrorCode string `json:"errorCode,omitempty"` State DeleteRecordOpenapiResponseStateEnum `json:"state,omitempty"` Body *[]DeleteRecordOpenapiResponseBody `json:"body,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/delete_record_openapi_response_body.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type DeleteRecordOpenapiResponseBodyCodeEnum string // List of Code const ( DeleteRecordOpenapiResponseBodyCodeEnumError DeleteRecordOpenapiResponseBodyCodeEnum = "ERROR" DeleteRecordOpenapiResponseBodyCodeEnumSuccess DeleteRecordOpenapiResponseBodyCodeEnum = "SUCCESS" ) type DeleteRecordOpenapiResponseBody struct { // 结果说明 Msg string `json:"msg,omitempty"` // 解析记录ID RecordId string `json:"recordId,omitempty"` // 结果码 Code DeleteRecordOpenapiResponseBodyCodeEnum `json:"code,omitempty"` // 域名 DomainName string `json:"domainName,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/delete_record_request.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type DeleteRecordRequest struct { DeleteRecordBody *DeleteRecordBody `json:"deleteRecordBody,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/delete_record_response.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type DeleteRecordResponseStateEnum string // List of State const ( DeleteRecordResponseStateEnumError DeleteRecordResponseStateEnum = "ERROR" DeleteRecordResponseStateEnumException DeleteRecordResponseStateEnum = "EXCEPTION" DeleteRecordResponseStateEnumForbidden DeleteRecordResponseStateEnum = "FORBIDDEN" DeleteRecordResponseStateEnumOk DeleteRecordResponseStateEnum = "OK" ) type DeleteRecordResponse struct { RequestId string `json:"requestId,omitempty"` ErrorMessage string `json:"errorMessage,omitempty"` ErrorCode string `json:"errorCode,omitempty"` State DeleteRecordResponseStateEnum `json:"state,omitempty"` Body *[]DeleteRecordResponseBody `json:"body,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/delete_record_response_body.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type DeleteRecordResponseBodyCodeEnum string // List of Code const ( DeleteRecordResponseBodyCodeEnumError DeleteRecordResponseBodyCodeEnum = "ERROR" DeleteRecordResponseBodyCodeEnumSuccess DeleteRecordResponseBodyCodeEnum = "SUCCESS" ) type DeleteRecordResponseBody struct { // 结果说明 Msg string `json:"msg,omitempty"` // 解析记录ID RecordId string `json:"recordId,omitempty"` // 结果码 Code DeleteRecordResponseBodyCodeEnum `json:"code,omitempty"` // 域名 DomainName string `json:"domainName,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_body.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model import ( "gitlab.ecloud.com/ecloud/ecloudsdkcore/position" ) type ListRecordBody struct { position.Body // 域名 DomainName string `json:"domainName"` // 可以匹配主机头rr、记录值value、备注description,并且是模糊搜索 DataLike string `json:"dataLike,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_openapi_body.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model import ( "gitlab.ecloud.com/ecloud/ecloudsdkcore/position" ) type ListRecordOpenapiBody struct { position.Body // 域名 DomainName string `json:"domainName"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_openapi_query.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model import ( "gitlab.ecloud.com/ecloud/ecloudsdkcore/position" ) type ListRecordOpenapiQuery struct { position.Query // 页大小 PageSize *int32 `json:"pageSize,omitempty"` // 当前页 Page *int32 `json:"page,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_openapi_request.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type ListRecordOpenapiRequest struct { ListRecordOpenapiQuery *ListRecordOpenapiQuery `json:"listRecordOpenapiQuery,omitempty"` ListRecordOpenapiBody *ListRecordOpenapiBody `json:"listRecordOpenapiBody,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_openapi_response.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type ListRecordOpenapiResponseStateEnum string // List of State const ( ListRecordOpenapiResponseStateEnumError ListRecordOpenapiResponseStateEnum = "ERROR" ListRecordOpenapiResponseStateEnumException ListRecordOpenapiResponseStateEnum = "EXCEPTION" ListRecordOpenapiResponseStateEnumForbidden ListRecordOpenapiResponseStateEnum = "FORBIDDEN" ListRecordOpenapiResponseStateEnumOk ListRecordOpenapiResponseStateEnum = "OK" ) type ListRecordOpenapiResponse struct { RequestId string `json:"requestId,omitempty"` ErrorMessage string `json:"errorMessage,omitempty"` ErrorCode string `json:"errorCode,omitempty"` State ListRecordOpenapiResponseStateEnum `json:"state,omitempty"` Body *ListRecordOpenapiResponseBody `json:"body,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_openapi_response_body.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type ListRecordOpenapiResponseBody struct { // 当前页的具体数据列表 Data *[]ListRecordOpenapiResponseData `json:"data,omitempty"` // 总数据量 TotalNum *int32 `json:"totalNum,omitempty"` // 总页数 TotalPages *int32 `json:"totalPages,omitempty"` // 页大小 PageSize *int32 `json:"pageSize,omitempty"` // 当前页码,从0开始,0表示第一页 Page *int32 `json:"page,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_openapi_response_data.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type ListRecordOpenapiResponseDataTypeEnum string // List of Type const ( ListRecordOpenapiResponseDataTypeEnumA ListRecordOpenapiResponseDataTypeEnum = "A" ListRecordOpenapiResponseDataTypeEnumAaaa ListRecordOpenapiResponseDataTypeEnum = "AAAA" ListRecordOpenapiResponseDataTypeEnumCname ListRecordOpenapiResponseDataTypeEnum = "CNAME" ListRecordOpenapiResponseDataTypeEnumMx ListRecordOpenapiResponseDataTypeEnum = "MX" ListRecordOpenapiResponseDataTypeEnumTxt ListRecordOpenapiResponseDataTypeEnum = "TXT" ListRecordOpenapiResponseDataTypeEnumNs ListRecordOpenapiResponseDataTypeEnum = "NS" ListRecordOpenapiResponseDataTypeEnumSpf ListRecordOpenapiResponseDataTypeEnum = "SPF" ListRecordOpenapiResponseDataTypeEnumSrv ListRecordOpenapiResponseDataTypeEnum = "SRV" ListRecordOpenapiResponseDataTypeEnumCaa ListRecordOpenapiResponseDataTypeEnum = "CAA" ListRecordOpenapiResponseDataTypeEnumCmauth ListRecordOpenapiResponseDataTypeEnum = "CMAUTH" ) type ListRecordOpenapiResponseDataTimedStatusEnum string // List of TimedStatus const ( ListRecordOpenapiResponseDataTimedStatusEnumDisabled ListRecordOpenapiResponseDataTimedStatusEnum = "DISABLED" ListRecordOpenapiResponseDataTimedStatusEnumEnabled ListRecordOpenapiResponseDataTimedStatusEnum = "ENABLED" ListRecordOpenapiResponseDataTimedStatusEnumTimed ListRecordOpenapiResponseDataTimedStatusEnum = "TIMED" ) type ListRecordOpenapiResponseDataStateEnum string // List of State const ( ListRecordOpenapiResponseDataStateEnumDisabled ListRecordOpenapiResponseDataStateEnum = "DISABLED" ListRecordOpenapiResponseDataStateEnumEnabled ListRecordOpenapiResponseDataStateEnum = "ENABLED" ) type ListRecordOpenapiResponseData struct { // 主机头 Rr string `json:"rr,omitempty"` // 修改时间 ModifiedTime string `json:"modifiedTime,omitempty"` // 线路中文名 LineZh string `json:"lineZh,omitempty"` // 备注 Description string `json:"description,omitempty"` // 线路ID LineId string `json:"lineId,omitempty"` // 权重值 Weight *int32 `json:"weight,omitempty"` // MX优先级 MxPri *int32 `json:"mxPri,omitempty"` // 记录类型 Type ListRecordOpenapiResponseDataTypeEnum `json:"type,omitempty"` // 缓存的生命周期 Ttl *int32 `json:"ttl,omitempty"` // 标签 Tags *[]ListRecordOpenapiResponseTags `json:"tags,omitempty"` // 解析记录ID RecordId string `json:"recordId,omitempty"` // 定时状态 TimedStatus ListRecordOpenapiResponseDataTimedStatusEnum `json:"timedStatus,omitempty"` // 域名名称 DomainName string `json:"domainName,omitempty"` // 线路英文名 LineEn string `json:"lineEn,omitempty"` // 状态 State ListRecordOpenapiResponseDataStateEnum `json:"state,omitempty"` // 记录值 Value string `json:"value,omitempty"` // 定时发布时间 Pubdate string `json:"pubdate,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_openapi_response_tags.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type ListRecordOpenapiResponseTags struct { // 标签ID TagId string `json:"tagId,omitempty"` // 标签名称 Value string `json:"value,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_query.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model import ( "gitlab.ecloud.com/ecloud/ecloudsdkcore/position" ) type ListRecordQuery struct { position.Query // 页大小 PageSize *int32 `json:"pageSize,omitempty"` // 当前页 CurrentPage *int32 `json:"currentPage,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_request.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type ListRecordRequest struct { ListRecordBody *ListRecordBody `json:"listRecordBody,omitempty"` ListRecordQuery *ListRecordQuery `json:"listRecordQuery,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_response.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type ListRecordResponseStateEnum string // List of State const ( ListRecordResponseStateEnumError ListRecordResponseStateEnum = "ERROR" ListRecordResponseStateEnumException ListRecordResponseStateEnum = "EXCEPTION" ListRecordResponseStateEnumForbidden ListRecordResponseStateEnum = "FORBIDDEN" ListRecordResponseStateEnumOk ListRecordResponseStateEnum = "OK" ) type ListRecordResponse struct { RequestId string `json:"requestId,omitempty"` ErrorMessage string `json:"errorMessage,omitempty"` ErrorCode string `json:"errorCode,omitempty"` State ListRecordResponseStateEnum `json:"state,omitempty"` Body *ListRecordResponseBody `json:"body,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_response_body.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type ListRecordResponseBody struct { // 总页数 TotalPages *int32 `json:"totalPages,omitempty"` // 当前页码,从0开始,0表示第一页 CurrentPage *int32 `json:"currentPage,omitempty"` // 当前页的具体数据列表 Results *[]ListRecordResponseResults `json:"results,omitempty"` // 总数据量 TotalElements *int64 `json:"totalElements,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/list_record_response_results.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type ListRecordResponseResultsTypeEnum string // List of Type const ( ListRecordResponseResultsTypeEnumA ListRecordResponseResultsTypeEnum = "A" ListRecordResponseResultsTypeEnumAaaa ListRecordResponseResultsTypeEnum = "AAAA" ListRecordResponseResultsTypeEnumCaa ListRecordResponseResultsTypeEnum = "CAA" ListRecordResponseResultsTypeEnumCmauth ListRecordResponseResultsTypeEnum = "CMAUTH" ListRecordResponseResultsTypeEnumCname ListRecordResponseResultsTypeEnum = "CNAME" ListRecordResponseResultsTypeEnumMx ListRecordResponseResultsTypeEnum = "MX" ListRecordResponseResultsTypeEnumNs ListRecordResponseResultsTypeEnum = "NS" ListRecordResponseResultsTypeEnumPtr ListRecordResponseResultsTypeEnum = "PTR" ListRecordResponseResultsTypeEnumRp ListRecordResponseResultsTypeEnum = "RP" ListRecordResponseResultsTypeEnumSpf ListRecordResponseResultsTypeEnum = "SPF" ListRecordResponseResultsTypeEnumSrv ListRecordResponseResultsTypeEnum = "SRV" ListRecordResponseResultsTypeEnumTxt ListRecordResponseResultsTypeEnum = "TXT" ListRecordResponseResultsTypeEnumUrl ListRecordResponseResultsTypeEnum = "URL" ) type ListRecordResponseResultsTimedStatusEnum string // List of TimedStatus const ( ListRecordResponseResultsTimedStatusEnumDisabled ListRecordResponseResultsTimedStatusEnum = "DISABLED" ListRecordResponseResultsTimedStatusEnumEnabled ListRecordResponseResultsTimedStatusEnum = "ENABLED" ListRecordResponseResultsTimedStatusEnumTimed ListRecordResponseResultsTimedStatusEnum = "TIMED" ) type ListRecordResponseResultsStateEnum string // List of State const ( ListRecordResponseResultsStateEnumDisabled ListRecordResponseResultsStateEnum = "DISABLED" ListRecordResponseResultsStateEnumEnabled ListRecordResponseResultsStateEnum = "ENABLED" ) type ListRecordResponseResults struct { // 主机头 Rr string `json:"rr,omitempty"` // 修改时间 ModifiedTime string `json:"modifiedTime,omitempty"` // 线路中文名 LineZh string `json:"lineZh,omitempty"` // 备注 Description string `json:"description,omitempty"` // 线路ID LineId string `json:"lineId,omitempty"` // 权重值 Weight *int32 `json:"weight,omitempty"` // MX优先级 MxPri *int32 `json:"mxPri,omitempty"` // 记录类型 Type ListRecordResponseResultsTypeEnum `json:"type,omitempty"` // 缓存的生命周期 Ttl *int32 `json:"ttl,omitempty"` // 解析记录ID RecordId string `json:"recordId,omitempty"` // 定时状态 TimedStatus ListRecordResponseResultsTimedStatusEnum `json:"timedStatus,omitempty"` // 域名名称 DomainName string `json:"domainName,omitempty"` // 线路英文名 LineEn string `json:"lineEn,omitempty"` // 状态 State ListRecordResponseResultsStateEnum `json:"state,omitempty"` // 记录值 Value string `json:"value,omitempty"` // 定时发布时间 Pubdate string `json:"pubdate,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/modify_record_body.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model import ( "gitlab.ecloud.com/ecloud/ecloudsdkcore/position" ) type ModifyRecordBodyTypeEnum string // List of Type const ( ModifyRecordBodyTypeEnumA ModifyRecordBodyTypeEnum = "A" ModifyRecordBodyTypeEnumAaaa ModifyRecordBodyTypeEnum = "AAAA" ModifyRecordBodyTypeEnumCaa ModifyRecordBodyTypeEnum = "CAA" ModifyRecordBodyTypeEnumCmauth ModifyRecordBodyTypeEnum = "CMAUTH" ModifyRecordBodyTypeEnumCname ModifyRecordBodyTypeEnum = "CNAME" ModifyRecordBodyTypeEnumMx ModifyRecordBodyTypeEnum = "MX" ModifyRecordBodyTypeEnumNs ModifyRecordBodyTypeEnum = "NS" ModifyRecordBodyTypeEnumPtr ModifyRecordBodyTypeEnum = "PTR" ModifyRecordBodyTypeEnumRp ModifyRecordBodyTypeEnum = "RP" ModifyRecordBodyTypeEnumSpf ModifyRecordBodyTypeEnum = "SPF" ModifyRecordBodyTypeEnumSrv ModifyRecordBodyTypeEnum = "SRV" ModifyRecordBodyTypeEnumTxt ModifyRecordBodyTypeEnum = "TXT" ModifyRecordBodyTypeEnumUrl ModifyRecordBodyTypeEnum = "URL" ) type ModifyRecordBody struct { position.Body // 解析记录ID RecordId string `json:"recordId"` // 主机头 Rr string `json:"rr,omitempty"` // 域名名称 DomainName string `json:"domainName"` // 备注 Description string `json:"description,omitempty"` // 线路ID LineId string `json:"lineId,omitempty"` // MX优先级 MxPri *int32 `json:"mxPri,omitempty"` // 记录类型 Type ModifyRecordBodyTypeEnum `json:"type,omitempty"` // 缓存的生命周期 Ttl *int32 `json:"ttl,omitempty"` // 记录值 Value string `json:"value,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/modify_record_openapi_body.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model import ( "gitlab.ecloud.com/ecloud/ecloudsdkcore/position" ) type ModifyRecordOpenapiBodyTypeEnum string // List of Type const ( ModifyRecordOpenapiBodyTypeEnumA ModifyRecordOpenapiBodyTypeEnum = "A" ModifyRecordOpenapiBodyTypeEnumAaaa ModifyRecordOpenapiBodyTypeEnum = "AAAA" ModifyRecordOpenapiBodyTypeEnumCname ModifyRecordOpenapiBodyTypeEnum = "CNAME" ModifyRecordOpenapiBodyTypeEnumMx ModifyRecordOpenapiBodyTypeEnum = "MX" ModifyRecordOpenapiBodyTypeEnumTxt ModifyRecordOpenapiBodyTypeEnum = "TXT" ModifyRecordOpenapiBodyTypeEnumNs ModifyRecordOpenapiBodyTypeEnum = "NS" ModifyRecordOpenapiBodyTypeEnumSpf ModifyRecordOpenapiBodyTypeEnum = "SPF" ModifyRecordOpenapiBodyTypeEnumSrv ModifyRecordOpenapiBodyTypeEnum = "SRV" ModifyRecordOpenapiBodyTypeEnumCaa ModifyRecordOpenapiBodyTypeEnum = "CAA" ModifyRecordOpenapiBodyTypeEnumCmauth ModifyRecordOpenapiBodyTypeEnum = "CMAUTH" ) type ModifyRecordOpenapiBody struct { position.Body // 解析记录ID RecordId string `json:"recordId"` // 主机头 Rr string `json:"rr,omitempty"` // 域名名称 DomainName string `json:"domainName"` // 备注 Description string `json:"description,omitempty"` // 线路ID LineId string `json:"lineId,omitempty"` // MX优先级 MxPri *int32 `json:"mxPri,omitempty"` // 记录类型 Type ModifyRecordOpenapiBodyTypeEnum `json:"type,omitempty"` // 缓存的生命周期 Ttl *int32 `json:"ttl,omitempty"` // 记录值 Value string `json:"value,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/modify_record_openapi_request.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type ModifyRecordOpenapiRequest struct { ModifyRecordOpenapiBody *ModifyRecordOpenapiBody `json:"modifyRecordOpenapiBody,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/modify_record_openapi_response.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type ModifyRecordOpenapiResponseStateEnum string // List of State const ( ModifyRecordOpenapiResponseStateEnumError ModifyRecordOpenapiResponseStateEnum = "ERROR" ModifyRecordOpenapiResponseStateEnumException ModifyRecordOpenapiResponseStateEnum = "EXCEPTION" ModifyRecordOpenapiResponseStateEnumForbidden ModifyRecordOpenapiResponseStateEnum = "FORBIDDEN" ModifyRecordOpenapiResponseStateEnumOk ModifyRecordOpenapiResponseStateEnum = "OK" ) type ModifyRecordOpenapiResponse struct { RequestId string `json:"requestId,omitempty"` ErrorMessage string `json:"errorMessage,omitempty"` ErrorCode string `json:"errorCode,omitempty"` State ModifyRecordOpenapiResponseStateEnum `json:"state,omitempty"` Body *ModifyRecordOpenapiResponseBody `json:"body,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/modify_record_openapi_response_body.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type ModifyRecordOpenapiResponseBodyTypeEnum string // List of Type const ( ModifyRecordOpenapiResponseBodyTypeEnumA ModifyRecordOpenapiResponseBodyTypeEnum = "A" ModifyRecordOpenapiResponseBodyTypeEnumAaaa ModifyRecordOpenapiResponseBodyTypeEnum = "AAAA" ModifyRecordOpenapiResponseBodyTypeEnumCname ModifyRecordOpenapiResponseBodyTypeEnum = "CNAME" ModifyRecordOpenapiResponseBodyTypeEnumMx ModifyRecordOpenapiResponseBodyTypeEnum = "MX" ModifyRecordOpenapiResponseBodyTypeEnumTxt ModifyRecordOpenapiResponseBodyTypeEnum = "TXT" ModifyRecordOpenapiResponseBodyTypeEnumNs ModifyRecordOpenapiResponseBodyTypeEnum = "NS" ModifyRecordOpenapiResponseBodyTypeEnumSpf ModifyRecordOpenapiResponseBodyTypeEnum = "SPF" ModifyRecordOpenapiResponseBodyTypeEnumSrv ModifyRecordOpenapiResponseBodyTypeEnum = "SRV" ModifyRecordOpenapiResponseBodyTypeEnumCaa ModifyRecordOpenapiResponseBodyTypeEnum = "CAA" ModifyRecordOpenapiResponseBodyTypeEnumCmauth ModifyRecordOpenapiResponseBodyTypeEnum = "CMAUTH" ) type ModifyRecordOpenapiResponseBodyTimedStatusEnum string // List of TimedStatus const ( ModifyRecordOpenapiResponseBodyTimedStatusEnumDisabled ModifyRecordOpenapiResponseBodyTimedStatusEnum = "DISABLED" ModifyRecordOpenapiResponseBodyTimedStatusEnumEnabled ModifyRecordOpenapiResponseBodyTimedStatusEnum = "ENABLED" ModifyRecordOpenapiResponseBodyTimedStatusEnumTimed ModifyRecordOpenapiResponseBodyTimedStatusEnum = "TIMED" ) type ModifyRecordOpenapiResponseBodyStateEnum string // List of State const ( ModifyRecordOpenapiResponseBodyStateEnumDisabled ModifyRecordOpenapiResponseBodyStateEnum = "DISABLED" ModifyRecordOpenapiResponseBodyStateEnumEnabled ModifyRecordOpenapiResponseBodyStateEnum = "ENABLED" ) type ModifyRecordOpenapiResponseBody struct { // 主机头 Rr string `json:"rr,omitempty"` // 修改时间 ModifiedTime string `json:"modifiedTime,omitempty"` // 线路中文名 LineZh string `json:"lineZh,omitempty"` // 备注 Description string `json:"description,omitempty"` // 线路ID LineId string `json:"lineId,omitempty"` // 权重值 Weight *int32 `json:"weight,omitempty"` // MX优先级 MxPri *int32 `json:"mxPri,omitempty"` // 记录类型 Type ModifyRecordOpenapiResponseBodyTypeEnum `json:"type,omitempty"` // 缓存的生命周期 Ttl *int32 `json:"ttl,omitempty"` // 标签 Tags *[]ModifyRecordOpenapiResponseTags `json:"tags,omitempty"` // 解析记录ID RecordId string `json:"recordId,omitempty"` // 定时状态 TimedStatus ModifyRecordOpenapiResponseBodyTimedStatusEnum `json:"timedStatus,omitempty"` // 域名名称 DomainName string `json:"domainName,omitempty"` // 线路英文名 LineEn string `json:"lineEn,omitempty"` // 状态 State ModifyRecordOpenapiResponseBodyStateEnum `json:"state,omitempty"` // 记录值 Value string `json:"value,omitempty"` // 定时发布时间 Pubdate string `json:"pubdate,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/modify_record_openapi_response_tags.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type ModifyRecordOpenapiResponseTags struct { // 标签ID TagId string `json:"tagId,omitempty"` // 标签名称 Value string `json:"value,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/modify_record_request.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type ModifyRecordRequest struct { ModifyRecordBody *ModifyRecordBody `json:"modifyRecordBody,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/modify_record_response.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type ModifyRecordResponseStateEnum string // List of State const ( ModifyRecordResponseStateEnumError ModifyRecordResponseStateEnum = "ERROR" ModifyRecordResponseStateEnumException ModifyRecordResponseStateEnum = "EXCEPTION" ModifyRecordResponseStateEnumForbidden ModifyRecordResponseStateEnum = "FORBIDDEN" ModifyRecordResponseStateEnumOk ModifyRecordResponseStateEnum = "OK" ) type ModifyRecordResponse struct { RequestId string `json:"requestId,omitempty"` ErrorMessage string `json:"errorMessage,omitempty"` ErrorCode string `json:"errorCode,omitempty"` State ModifyRecordResponseStateEnum `json:"state,omitempty"` Body *ModifyRecordResponseBody `json:"body,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkclouddns@v1.0.1/model/modify_record_response_body.go ================================================ // @Title Golang SDK Client // @Description This code is auto generated // @Author Ecloud SDK package model type ModifyRecordResponseBodyTypeEnum string // List of Type const ( ModifyRecordResponseBodyTypeEnumA ModifyRecordResponseBodyTypeEnum = "A" ModifyRecordResponseBodyTypeEnumAaaa ModifyRecordResponseBodyTypeEnum = "AAAA" ModifyRecordResponseBodyTypeEnumCaa ModifyRecordResponseBodyTypeEnum = "CAA" ModifyRecordResponseBodyTypeEnumCmauth ModifyRecordResponseBodyTypeEnum = "CMAUTH" ModifyRecordResponseBodyTypeEnumCname ModifyRecordResponseBodyTypeEnum = "CNAME" ModifyRecordResponseBodyTypeEnumMx ModifyRecordResponseBodyTypeEnum = "MX" ModifyRecordResponseBodyTypeEnumNs ModifyRecordResponseBodyTypeEnum = "NS" ModifyRecordResponseBodyTypeEnumPtr ModifyRecordResponseBodyTypeEnum = "PTR" ModifyRecordResponseBodyTypeEnumRp ModifyRecordResponseBodyTypeEnum = "RP" ModifyRecordResponseBodyTypeEnumSpf ModifyRecordResponseBodyTypeEnum = "SPF" ModifyRecordResponseBodyTypeEnumSrv ModifyRecordResponseBodyTypeEnum = "SRV" ModifyRecordResponseBodyTypeEnumTxt ModifyRecordResponseBodyTypeEnum = "TXT" ModifyRecordResponseBodyTypeEnumUrl ModifyRecordResponseBodyTypeEnum = "URL" ) type ModifyRecordResponseBodyStateEnum string // List of State const ( ModifyRecordResponseBodyStateEnumDisabled ModifyRecordResponseBodyStateEnum = "DISABLED" ModifyRecordResponseBodyStateEnumEnabled ModifyRecordResponseBodyStateEnum = "ENABLED" ) type ModifyRecordResponseBody struct { // 主机头 Rr string `json:"rr,omitempty"` // 修改时间 ModifiedTime string `json:"modifiedTime,omitempty"` // 线路中文名 LineZh string `json:"lineZh,omitempty"` // 备注 Description string `json:"description,omitempty"` // 线路ID LineId string `json:"lineId,omitempty"` // 权重值 Weight *int32 `json:"weight,omitempty"` // MX优先级 MxPri *int32 `json:"mxPri,omitempty"` // 记录类型 Type ModifyRecordResponseBodyTypeEnum `json:"type,omitempty"` // 缓存的生命周期 Ttl *int32 `json:"ttl,omitempty"` // 解析记录ID RecordId string `json:"recordId,omitempty"` // 域名名称 DomainName string `json:"domainName,omitempty"` // 线路英文名 LineEn string `json:"lineEn,omitempty"` // 状态 State ModifyRecordResponseBodyStateEnum `json:"state,omitempty"` // 记录值 Value string `json:"value,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkcore@v1.0.0/api_client.go ================================================ package ecloudsdkcore import ( "bytes" "encoding/json" "encoding/xml" "errors" "fmt" "io" "io/ioutil" "mime/multipart" "net/http" "net/url" "os" "path/filepath" "reflect" "regexp" "strconv" "strings" "time" "unicode/utf8" "gitlab.ecloud.com/ecloud/ecloudsdkcore/config" ) var ( jsonCheck = regexp.MustCompile("(?i:(?:application|text)/json)") xmlCheck = regexp.MustCompile("(?i:(?:application|text)/xml)") ) // APIClient manages communication // In most cases there should be only one, shared, APIClient. type APIClient struct { cfg *Configuration common service } type service struct { client *APIClient } type HttpRequestPosition string const ( BODY HttpRequestPosition = "Body" QUERY HttpRequestPosition = "Query" PATH HttpRequestPosition = "Path" HEADER HttpRequestPosition = "Header" ) const ( SdkPortalUrl = "/op-apim-portal/apim/request/sdk" SdkPortalGatewayUrl = "/api/query/openapi/apim/request/sdk" ) // NewAPIClient creates a new API client. func NewAPIClient() *APIClient { cfg := NewConfiguration() if cfg.HTTPClient == nil { cfg.HTTPClient = http.DefaultClient } c := &APIClient{} c.cfg = cfg c.common.client = c return c } // atoi string to int func atoi(in string) (int, error) { return strconv.Atoi(in) } // selectHeaderContentType select a content type from the available list. func selectHeaderContentType(contentTypes []string) string { if len(contentTypes) == 0 { return "" } if contains(contentTypes, "application/json") { return "application/json" } return contentTypes[0] } // selectHeaderAccept join all accept types and return func selectHeaderAccept(accepts []string) string { if len(accepts) == 0 { return "" } if contains(accepts, "application/json") { return "application/json" } return strings.Join(accepts, ",") } // contains is a case insenstive match, finding needle in a haystack func contains(haystack []string, needle string) bool { for _, a := range haystack { if strings.ToLower(a) == strings.ToLower(needle) { return true } } return false } // Verify optional parameters are of the correct type. func typeCheckParameter(obj interface{}, expected string, name string) error { if obj == nil { return nil } if reflect.TypeOf(obj).String() != expected { return fmt.Errorf("Expected %s to be of type %s but received %s.", name, expected, reflect.TypeOf(obj).String()) } return nil } // parameterToString convert interface{} parameters to string, using a delimiter if format is provided. func parameterToString(obj interface{}, collectionFormat string, request HttpRequest) (*http.Request, string) { var delimiter string switch collectionFormat { case "pipes": delimiter = "|" case "ssv": delimiter = " " case "tsv": delimiter = "\t" case "csv": delimiter = "," } if reflect.TypeOf(obj).Kind() == reflect.Slice { return nil, strings.Trim(strings.Replace(fmt.Sprint(obj), " ", delimiter, -1), "[]") } return nil, fmt.Sprintf("%v", obj) } // Excute entry for http call func (c *APIClient) Excute(httpRequest *HttpRequest, config *config.Config, returnType interface{}) (*http.Response, error) { httpRequest = buildHttpRequest(httpRequest, config) request := buildCall(httpRequest) httpResponse, err := c.callAPI(request) if err != nil || httpResponse == nil { return nil, err } responseBody, err := ioutil.ReadAll(httpResponse.Body) httpResponse.Body.Close() if err != nil { return httpResponse, err } if httpResponse.StatusCode < 300 { // If we succeed, return the data, otherwise pass on to decode error. err = c.decode(&returnType, responseBody, httpResponse.Header.Get("Content-Type")) if err != nil { return httpResponse, fmt.Errorf("%w, response body is: %s", err, string(responseBody)) } return httpResponse, nil } if httpResponse.StatusCode >= 300 { newErr := GenericResponseError{ body: responseBody, error: httpResponse.Status, } return httpResponse, newErr } return httpResponse, err } // callAPI do the request. func (c *APIClient) callAPI(request *http.Request) (*http.Response, error) { return c.cfg.HTTPClient.Do(request) } // ChangeBasePath Change base path to allow switching to mocks func (c *APIClient) ChangeBasePath(path string) { c.cfg.BasePath = path } // buildHttpRequest build the request func buildHttpRequest(httpRequest *HttpRequest, config *config.Config) *HttpRequest { openApiRequest := &OpenApiRequest{ AccessKey: config.AccessKey, SecretKey: config.SecretKey, PoolId: config.PoolId, Api: httpRequest.Action, Product: httpRequest.Product, Version: httpRequest.Version, SdkVersion: httpRequest.SdkVersion, Language: "Golang", } if httpRequest.Body != nil { reqType := reflect.TypeOf(httpRequest.Body) if reqType.Kind() == reflect.Ptr { reqType = reqType.Elem() } v := reflect.ValueOf(httpRequest.Body) if v.Kind() == reflect.Ptr { v = v.Elem() } flag := false for i := 0; i < reqType.NumField(); i++ { fieldType := reqType.Field(i) value := v.FieldByName(fieldType.Name) if value.Kind() == reflect.Ptr { if value.IsNil() { continue } value = value.Elem() } propertyType := fieldType.Type if propertyType.Kind() == reflect.Ptr { propertyType = propertyType.Elem() } _, flag = propertyType.FieldByName(string(BODY)) if flag { openApiRequest.BodyParameter = value.Interface() continue } _, flag = propertyType.FieldByName(string(HEADER)) if flag { openApiRequest.HeaderParameter = structToMap(value.Interface()) continue } _, flag = propertyType.FieldByName(string(QUERY)) if flag { openApiRequest.QueryParameter = structToMap(value.Interface()) continue } _, flag = propertyType.FieldByName(string(PATH)) if flag { openApiRequest.PathParameter = structToMap(value.Interface()) continue } } } headers := make(map[string]interface{}) if httpRequest.HeaderParams != nil { if openApiRequest.HeaderParameter == nil { headers = httpRequest.HeaderParams } else { headers = mergeMap(openApiRequest.HeaderParameter, httpRequest.HeaderParams) } openApiRequest.HeaderParameter = headers } httpRequest.Body = openApiRequest return httpRequest } // mergeMap merge the two map results func mergeMap(mObj ...map[string]interface{}) map[string]interface{} { newMap := map[string]interface{}{} for _, m := range mObj { for k, v := range m { newMap[k] = v } } return newMap } // structToMap struct convert to map func structToMap(value interface{}) map[string]interface{} { data, _ := json.Marshal(value) result := make(map[string]interface{}) json.Unmarshal(data, &result) return result } func buildCall(httpRequest *HttpRequest) (request *http.Request) { url := "" if len(httpRequest.Url) > 0 { url = httpRequest.Url + SdkPortalUrl } else { url = httpRequest.DefaultUrl + SdkPortalGatewayUrl } request, _ = prepareRequest(url, "POST", httpRequest.Body) return request } // prepareRequest build the request func prepareRequest(path string, method string, postBody interface{}, ) (httpRequest *http.Request, err error) { var body *bytes.Buffer // Detect postBody type and post. if postBody != nil { contentType := detectContentType(postBody) body, err = setBody(postBody, contentType) if err != nil { return nil, err } } // Setup path and query parameters url, err := url.Parse(path) if err != nil { return nil, err } // Generate a new request if body != nil { httpRequest, err = http.NewRequest(method, url.String(), body) } else { httpRequest, err = http.NewRequest(method, url.String(), nil) } if err != nil { return nil, err } // add default header parameters httpRequest.Header.Add("Content-Type", "application/json") return httpRequest, nil } func (c *APIClient) decode(v interface{}, b []byte, contentType string) (err error) { if strings.Contains(contentType, "application/xml") { if err = xml.Unmarshal(b, v); err != nil { return err } return nil } else if strings.Contains(contentType, "application/json") { platformResponse := &APIPlatformResponse{} if err = json.Unmarshal(b, platformResponse); err != nil { newErr := GenericResponseError{ body: b, error: err.Error(), } return newErr } platformResponseBodyBytes, _ := json.Marshal(platformResponse.Body) platformResponseBody := &APIPlatformResponseBody{} if err = json.Unmarshal(platformResponseBodyBytes, platformResponseBody); err != nil { return err } /* 找到两层指针指向的元素 */ value := reflect.ValueOf(v).Elem().Elem() if !value.IsNil() { structValue := value.Elem() if structValue.NumField() == 1 && structValue.Field(0).Kind() == reflect.String { n := len(platformResponseBody.ResponseBody) structValue.Field(0).SetString(platformResponseBody.ResponseBody[1 : n-1]) return nil } } if err = json.Unmarshal([]byte(platformResponseBody.ResponseBody), v); err != nil { return err } return nil } return errors.New("undefined response type") } // Add a file to the multipart request func addFile(w *multipart.Writer, fieldName, path string) error { file, err := os.Open(path) if err != nil { return err } defer file.Close() part, err := w.CreateFormFile(fieldName, filepath.Base(path)) if err != nil { return err } _, err = io.Copy(part, file) return err } // Prevent trying to import "fmt" func reportError(format string, a ...interface{}) error { return fmt.Errorf(format, a...) } // Set request body from an interface{} func setBody(body interface{}, contentType string) (bodyBuf *bytes.Buffer, err error) { if bodyBuf == nil { bodyBuf = &bytes.Buffer{} } if reader, ok := body.(io.Reader); ok { _, err = bodyBuf.ReadFrom(reader) } else if b, ok := body.([]byte); ok { _, err = bodyBuf.Write(b) } else if s, ok := body.(string); ok { _, err = bodyBuf.WriteString(s) } else if s, ok := body.(*string); ok { _, err = bodyBuf.WriteString(*s) } else if jsonCheck.MatchString(contentType) { err = json.NewEncoder(bodyBuf).Encode(body) } else if xmlCheck.MatchString(contentType) { xml.NewEncoder(bodyBuf).Encode(body) } if err != nil { return nil, err } if bodyBuf.Len() == 0 { err = fmt.Errorf("Invalid body type %s\n", contentType) return nil, err } return bodyBuf, nil } // detectContentType method is used to figure out `Request.Body` content type for request header func detectContentType(body interface{}) string { contentType := "text/plain; charset=utf-8" kind := reflect.TypeOf(body).Kind() switch kind { case reflect.Struct, reflect.Map, reflect.Ptr: contentType = "application/json; charset=utf-8" case reflect.String: contentType = "text/plain; charset=utf-8" default: if b, ok := body.([]byte); ok { contentType = http.DetectContentType(b) } else if kind == reflect.Slice { contentType = "application/json; charset=utf-8" } } return contentType } type cacheControl map[string]string func parseCacheControl(headers http.Header) cacheControl { cc := cacheControl{} ccHeader := headers.Get("Cache-Control") for _, part := range strings.Split(ccHeader, ",") { part = strings.Trim(part, " ") if part == "" { continue } if strings.ContainsRune(part, '=') { keyval := strings.Split(part, "=") cc[strings.Trim(keyval[0], " ")] = strings.Trim(keyval[1], ",") } else { cc[part] = "" } } return cc } // CacheExpires helper function to determine remaining time before repeating a request. func CacheExpires(r *http.Response) time.Time { // Figure out when the cache expires. var expires time.Time now, err := time.Parse(time.RFC1123, r.Header.Get("date")) if err != nil { return time.Now() } respCacheControl := parseCacheControl(r.Header) if maxAge, ok := respCacheControl["max-age"]; ok { lifetime, err := time.ParseDuration(maxAge + "s") if err != nil { expires = now } expires = now.Add(lifetime) } else { expiresHeader := r.Header.Get("Expires") if expiresHeader != "" { expires, err = time.Parse(time.RFC1123, expiresHeader) if err != nil { expires = now } } } return expires } func strlen(s string) int { return utf8.RuneCountInString(s) } // GenericResponseError Provides access to the body, error and model on returned errors. type GenericResponseError struct { body []byte error string model interface{} } // Error returns non-empty string if there was an error. func (e GenericResponseError) Error() string { return e.error } // Body returns the raw bytes of the response func (e GenericResponseError) Body() []byte { return e.body } // Model returns the unpacked model of the error func (e GenericResponseError) Model() interface{} { return e.model } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkcore@v1.0.0/api_response.go ================================================ package ecloudsdkcore import ( "net/http" ) type ReturnState string const ( OK ReturnState = "OK" ERROR ReturnState = "ERROR" EXCEPTION ReturnState = "EXCEPTION" ALARM ReturnState = "ALARM" FORBIDDEN ReturnState = "FORBIDDEN" ) type APIResponse struct { *http.Response `json:"-"` Message string `json:"message,omitempty"` // Operation is the name of the swagger operation. Operation string `json:"operation,omitempty"` // RequestURL is the request URL. This value is always available, even if the // embedded *http.Response is nil. RequestURL string `json:"url,omitempty"` // Method is the HTTP method used for the request. This value is always // available, even if the embedded *http.Response is nil. Method string `json:"method,omitempty"` // Payload holds the contents of the response body (which may be nil or empty). // This is provided here as the raw response.Body() reader will have already // been drained. Payload []byte `json:"-"` } type APIPlatformResponse struct { RequestId string `json:"requestId,omitempty"` State ReturnState `json:"state,omitempty"` Body interface{} `json:"body,omitempty"` ErrorCode string `json:"errorCode,omitempty"` ErrorParams []string `json:"errorParams,omitempty"` ErrorMessage string `json:"errorMessage,omitempty"` } type APIPlatformResponseBody struct { // TimeConsuming int64 `json:"timeConsuming,omitempty"` ResponseBody string `json:"responseBody,omitempty"` RequestHeader map[string]interface{} `json:"requestHeader,omitempty"` ResponseHeader map[string]interface{} `json:"responseHeader,omitempty"` ResponseMessage string `json:"responseMessage,omitempty"` StatusCode int `json:"statusCode,omitempty"` HttpMethod string `json:"httpMethod,omitempty"` RequestUrl string `json:"requestUrl,omitempty"` } func NewAPIResponse(r *http.Response) *APIResponse { response := &APIResponse{Response: r} return response } func NewAPIResponseWithError(errorMessage string) *APIResponse { response := &APIResponse{Message: errorMessage} return response } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkcore@v1.0.0/config/config.go ================================================ package config type Config struct { AccessKey string `json:"accessKey,string"` SecretKey string `json:"secretKey,string"` PoolId string `json:"poolId,string"` ReadTimeOut int `json:"readTimeOut,int"` ConnectTimeout int `json:"connectTimeout,int"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkcore@v1.0.0/configuration.go ================================================ package ecloudsdkcore import ( "net/http" ) type APIKey struct { Key string Prefix string } type Configuration struct { BasePath string `json:"basePath,omitempty"` Host string `json:"host,omitempty"` Scheme string `json:"scheme,omitempty"` DefaultHeader map[string]string `json:"defaultHeader,omitempty"` UserAgent string `json:"userAgent,omitempty"` HTTPClient *http.Client } func NewConfiguration() *Configuration { cfg := &Configuration{ BasePath: "https://ecloud.10086.cn/", DefaultHeader: make(map[string]string), UserAgent: "Ecloud-SDK/1.0.0/go", } return cfg } func (c *Configuration) AddDefaultHeader(key string, value string) { c.DefaultHeader[key] = value } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkcore@v1.0.0/go.mod ================================================ module gitlab.ecloud.com/ecloud/ecloudsdkcore go 1.14 ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkcore@v1.0.0/http_request.go ================================================ package ecloudsdkcore type HttpRequest struct { Url string `json:"url,omitempty"` DefaultUrl string `json:"defaultUrl,omitempty"` Method string `json:"method,omitempty"` Action string `json:"action,omitempty"` Product string `json:"product,omitempty"` Version string `json:"version,omitempty"` SdkVersion string `json:"sdkVersion,omitempty"` Body interface{} `json:"body,omitempty"` PathParams map[string]interface{} `json:"pathParams,omitempty"` QueryParams map[string]interface{} `json:"queryParams,omitempty"` HeaderParams map[string]interface{} `json:"headerParams,omitempty"` } func NewDefaultHttpRequest() *HttpRequest { return &HttpRequest{ DefaultUrl: "https://ecloud.10086.cn", Method: "POST", } } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkcore@v1.0.0/open_api_request.go ================================================ package ecloudsdkcore type OpenApiRequest struct { Product string `json:"product,omitempty"` Version string `json:"version,omitempty"` SdkVersion string `json:"sdkVersion,omitempty"` Language string `json:"language,omitempty"` Api string `json:"api,omitempty"` PoolId string `json:"poolId,omitempty"` HeaderParameter map[string]interface{} `json:"headerParameter,omitempty"` PathParameter map[string]interface{} `json:"pathParameter,omitempty"` QueryParameter map[string]interface{} `json:"queryParameter,omitempty"` BodyParameter interface{} `json:"bodyParameter,omitempty"` AccessKey string `json:"accessKey,omitempty"` SecretKey string `json:"secretKey,omitempty"` } ================================================ FILE: pkg/forks/gitlab.ecloud.com/ecloud/ecloudsdkcore@v1.0.0/position/http_position.go ================================================ package position type Body struct{} type Query struct{} type Path struct{} type Header struct{} ================================================ FILE: pkg/logging/handler.go ================================================ package logging import ( "context" "log/slog" "sync" ) type HookHandlerOptions struct { Level slog.Leveler WriteFunc func(ctx context.Context, record Record) error } var _ slog.Handler = (*HookHandler)(nil) type HookHandler struct { mutex *sync.Mutex parent *HookHandler options *HookHandlerOptions group string attrs []slog.Attr } func NewHookHandler(opts *HookHandlerOptions) *HookHandler { if opts == nil { opts = &HookHandlerOptions{} } h := &HookHandler{ mutex: &sync.Mutex{}, options: opts, } if h.options.WriteFunc == nil { panic("the `options.WriteFunc` is nil") } if h.options.Level == nil { h.options.Level = slog.LevelInfo } return h } func (h *HookHandler) Enabled(ctx context.Context, level slog.Level) bool { return level >= h.options.Level.Level() } func (h *HookHandler) WithGroup(name string) slog.Handler { if name == "" { return h } return &HookHandler{ parent: h, mutex: h.mutex, options: h.options, group: name, } } func (h *HookHandler) WithAttrs(attrs []slog.Attr) slog.Handler { if len(attrs) == 0 { return h } return &HookHandler{ parent: h, mutex: h.mutex, options: h.options, attrs: attrs, } } func (h *HookHandler) Handle(ctx context.Context, r slog.Record) error { if h.group != "" { h.mutex.Lock() attrs := make([]any, 0, len(h.attrs)+r.NumAttrs()) for _, a := range h.attrs { attrs = append(attrs, a) } h.mutex.Unlock() r.Attrs(func(a slog.Attr) bool { attrs = append(attrs, a) return true }) r = slog.NewRecord(r.Time, r.Level, r.Message, r.PC) r.AddAttrs(slog.Group(h.group, attrs...)) } else if len(h.attrs) > 0 { r = r.Clone() h.mutex.Lock() r.AddAttrs(h.attrs...) h.mutex.Unlock() } if h.parent != nil { return h.parent.Handle(ctx, r) } if err := h.writeRecord(ctx, Record{Record: r}); err != nil { return err } return nil } func (h *HookHandler) SetLevel(level slog.Level) { h.mutex.Lock() h.options.Level = level h.mutex.Unlock() } func (h *HookHandler) writeRecord(ctx context.Context, r Record) error { if h.parent != nil { return h.parent.writeRecord(ctx, r) } return h.options.WriteFunc(ctx, r) } ================================================ FILE: pkg/logging/record.go ================================================ package logging import ( "log/slog" types "github.com/pocketbase/pocketbase/tools/types" ) type Record struct { slog.Record } func (r Record) Data() types.JSONMap[any] { data := make(map[string]any, r.NumAttrs()) r.Attrs(func(a slog.Attr) bool { if err := r.resolveAttr(data, a); err != nil { return false } return true }) return types.JSONMap[any](data) } func (r Record) resolveAttr(data map[string]any, attr slog.Attr) error { attr.Value = attr.Value.Resolve() if attr.Equal(slog.Attr{}) { return nil } switch attr.Value.Kind() { case slog.KindGroup: { attrs := attr.Value.Group() if len(attrs) == 0 { return nil } groupData := make(map[string]any, len(attrs)) for _, subAttr := range attrs { r.resolveAttr(groupData, subAttr) } if len(groupData) > 0 { data[attr.Key] = groupData } } default: { switch v := attr.Value.Any().(type) { case error: data[attr.Key] = v.Error() default: data[attr.Key] = v } } } return nil } ================================================ FILE: pkg/sdk3rd/1panel/api_settings_ssl_update.go ================================================ package onepanel import ( "context" "net/http" ) type SettingsSSLUpdateRequest struct { Cert string `json:"cert"` Key string `json:"key"` SSLType string `json:"sslType"` SSL string `json:"ssl"` SSLID int64 `json:"sslID"` AutoRestart string `json:"autoRestart"` } type SettingsSSLUpdateResponse struct { sdkResponseBase } func (c *Client) SettingsSSLUpdate(req *SettingsSSLUpdateRequest) (*SettingsSSLUpdateResponse, error) { return c.SettingsSSLUpdateWithContext(context.Background(), req) } func (c *Client) SettingsSSLUpdateWithContext(ctx context.Context, req *SettingsSSLUpdateRequest) (*SettingsSSLUpdateResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/settings/ssl/update") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &SettingsSSLUpdateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/1panel/api_website_get.go ================================================ package onepanel import ( "context" "fmt" "net/http" ) type WebsiteGetRequest struct { Name string `json:"name"` Type string `json:"type"` Page int32 `json:"page"` PageSize int32 `json:"pageSize"` } type WebsiteGetResponse struct { sdkResponseBase Data *struct { ID int64 `json:"id"` Alias string `json:"alias"` PrimaryDomain string `json:"primaryDomain"` Protocol string `json:"protocol"` Type string `json:"type"` Status string `json:"status"` SitePath string `json:"sitePath"` Remark string `json:"remark"` Domains []*struct { ID int64 `json:"id"` Domain string `json:"domain"` Port int32 `json:"port"` SSL bool `json:"ssl"` UpdatedAt string `json:"updatedAt"` CreatedAt string `json:"createdAt"` } `json:"domains"` WebsiteSSLId int64 `json:"webSiteSSLId"` UpdatedAt string `json:"updatedAt"` CreatedAt string `json:"createdAt"` } `json:"data,omitempty"` } func (c *Client) WebsiteGet(websiteId int64) (*WebsiteGetResponse, error) { return c.WebsiteGetWithContext(context.Background(), websiteId) } func (c *Client) WebsiteGetWithContext(ctx context.Context, websiteId int64) (*WebsiteGetResponse, error) { if websiteId == 0 { return nil, fmt.Errorf("sdkerr: unset websiteId") } httpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf("/websites/%d", websiteId)) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &WebsiteGetResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/1panel/api_website_https_get.go ================================================ package onepanel import ( "context" "fmt" "net/http" ) type WebsiteHttpsGetResponse struct { sdkResponseBase Data *struct { Enable bool `json:"enable"` WebsiteSSLID int64 `json:"websiteSSLId"` HttpConfig string `json:"httpConfig"` SSLProtocol []string `json:"SSLProtocol"` Algorithm string `json:"algorithm"` Hsts bool `json:"hsts"` } `json:"data,omitempty"` } func (c *Client) WebsiteHttpsGet(websiteId int64) (*WebsiteHttpsGetResponse, error) { return c.WebsiteHttpsGetWithContext(context.Background(), websiteId) } func (c *Client) WebsiteHttpsGetWithContext(ctx context.Context, websiteId int64) (*WebsiteHttpsGetResponse, error) { if websiteId == 0 { return nil, fmt.Errorf("sdkerr: unset websiteId") } httpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf("/websites/%d/https", websiteId)) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &WebsiteHttpsGetResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/1panel/api_website_https_post.go ================================================ package onepanel import ( "context" "fmt" "net/http" ) type WebsiteHttpsPostRequest struct { WebsiteID int64 `json:"websiteId"` Enable bool `json:"enable"` Type string `json:"type"` WebsiteSSLID int64 `json:"websiteSSLId"` HttpConfig string `json:"httpConfig"` SSLProtocol []string `json:"SSLProtocol"` Algorithm string `json:"algorithm"` Hsts bool `json:"hsts"` } type WebsiteHttpsPostResponse struct { sdkResponseBase } func (c *Client) WebsiteHttpsPost(websiteId int64, req *WebsiteHttpsPostRequest) (*WebsiteHttpsPostResponse, error) { return c.WebsiteHttpsPostWithContext(context.Background(), websiteId, req) } func (c *Client) WebsiteHttpsPostWithContext(ctx context.Context, websiteId int64, req *WebsiteHttpsPostRequest) (*WebsiteHttpsPostResponse, error) { if websiteId == 0 { return nil, fmt.Errorf("sdkerr: unset websiteId") } httpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf("/websites/%d/https", websiteId)) if err != nil { return nil, err } else { req.WebsiteID = websiteId httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &WebsiteHttpsPostResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/1panel/api_website_search.go ================================================ package onepanel import ( "context" "net/http" ) type WebsiteSearchRequest struct { Name string `json:"name"` Type string `json:"type"` Order string `json:"order"` OrderBy string `json:"orderBy"` Page int32 `json:"page"` PageSize int32 `json:"pageSize"` } type WebsiteSearchResponse struct { sdkResponseBase Data *struct { Items []*struct { ID int64 `json:"id"` Alias string `json:"alias"` PrimaryDomain string `json:"primaryDomain"` Protocol string `json:"protocol"` Type string `json:"type"` Status string `json:"status"` SitePath string `json:"sitePath"` Remark string `json:"remark"` SSLStatus string `json:"sslStatus"` SSLExpireDate string `json:"sslExpireDate"` UpdatedAt string `json:"updatedAt"` CreatedAt string `json:"createdAt"` } `json:"items"` Total int32 `json:"total"` } `json:"data,omitempty"` } func (c *Client) WebsiteSearch(req *WebsiteSearchRequest) (*WebsiteSearchResponse, error) { return c.WebsiteSearchWithContext(context.Background(), req) } func (c *Client) WebsiteSearchWithContext(ctx context.Context, req *WebsiteSearchRequest) (*WebsiteSearchResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/websites/search") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &WebsiteSearchResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/1panel/api_website_ssl_get.go ================================================ package onepanel import ( "context" "fmt" "net/http" ) type WebsiteSSLGetResponse struct { sdkResponseBase Data *struct { ID int64 `json:"id"` Provider string `json:"provider"` Description string `json:"description"` PrimaryDomain string `json:"primaryDomain"` Domains string `json:"domains"` Type string `json:"type"` Organization string `json:"organization"` Status string `json:"status"` StartDate string `json:"startDate"` ExpireDate string `json:"expireDate"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` } `json:"data,omitempty"` } func (c *Client) WebsiteSSLGet(sslId int64) (*WebsiteSSLGetResponse, error) { return c.WebsiteSSLGetWithContext(context.Background(), sslId) } func (c *Client) WebsiteSSLGetWithContext(ctx context.Context, sslId int64) (*WebsiteSSLGetResponse, error) { if sslId == 0 { return nil, fmt.Errorf("sdkerr: unset sslId") } httpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf("/websites/ssl/%d", sslId)) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &WebsiteSSLGetResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/1panel/api_website_ssl_search.go ================================================ package onepanel import ( "context" "net/http" ) type WebsiteSSLSearchRequest struct { Domain string `json:"domain"` Page int32 `json:"page"` PageSize int32 `json:"pageSize"` } type WebsiteSSLSearchResponse struct { sdkResponseBase Data *struct { Items []*struct { ID int64 `json:"id"` PEM string `json:"pem"` PrivateKey string `json:"privateKey"` Domains string `json:"domains"` Description string `json:"description"` Status string `json:"status"` UpdatedAt string `json:"updatedAt"` CreatedAt string `json:"createdAt"` } `json:"items"` Total int32 `json:"total"` } `json:"data,omitempty"` } func (c *Client) WebsiteSSLSearch(req *WebsiteSSLSearchRequest) (*WebsiteSSLSearchResponse, error) { return c.WebsiteSSLSearchWithContext(context.Background(), req) } func (c *Client) WebsiteSSLSearchWithContext(ctx context.Context, req *WebsiteSSLSearchRequest) (*WebsiteSSLSearchResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/websites/ssl/search") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &WebsiteSSLSearchResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/1panel/api_website_ssl_upload.go ================================================ package onepanel import ( "context" "net/http" ) type WebsiteSSLUploadRequest struct { SSLID int64 `json:"sslID"` Type string `json:"type"` Certificate string `json:"certificate"` CertificatePath string `json:"certificatePath"` PrivateKey string `json:"privateKey"` PrivateKeyPath string `json:"privateKeyPath"` Description string `json:"description"` } type WebsiteSSLUploadResponse struct { sdkResponseBase } func (c *Client) WebsiteSSLUpload(req *WebsiteSSLUploadRequest) (*WebsiteSSLUploadResponse, error) { return c.WebsiteSSLUploadWithContext(context.Background(), req) } func (c *Client) WebsiteSSLUploadWithContext(ctx context.Context, req *WebsiteSSLUploadRequest) (*WebsiteSSLUploadResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/websites/ssl/upload") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &WebsiteSSLUploadResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/1panel/client.go ================================================ package onepanel import ( "crypto/md5" "crypto/tls" "encoding/hex" "encoding/json" "fmt" "net/http" "net/url" "strings" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { client *resty.Client } func NewClient(serverUrl, apiKey string) (*Client, error) { if serverUrl == "" { return nil, fmt.Errorf("sdkerr: unset serverUrl") } if _, err := url.Parse(serverUrl); err != nil { return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err) } if apiKey == "" { return nil, fmt.Errorf("sdkerr: unset apiKey") } client := resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")+"/api/v1"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { timestamp := fmt.Sprintf("%d", time.Now().Unix()) tokenMd5 := md5.Sum([]byte("1panel" + apiKey + timestamp)) tokenMd5Hex := hex.EncodeToString(tokenMd5[:]) req.Header.Set("1Panel-Timestamp", timestamp) req.Header.Set("1Panel-Token", tokenMd5Hex) return nil }) return &Client{client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) SetTLSConfig(config *tls.Config) *Client { c.client.SetTLSClientConfig(config) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if tcode := res.GetCode(); tcode/100 != 2 { return resp, fmt.Errorf("sdkerr: api error: code='%d', message='%s'", tcode, res.GetMessage()) } } } return resp, nil } ================================================ FILE: pkg/sdk3rd/1panel/types.go ================================================ package onepanel type sdkResponse interface { GetCode() int GetMessage() string } type sdkResponseBase struct { Code *int `json:"code,omitempty"` Message *string `json:"message,omitempty"` } func (r *sdkResponseBase) GetCode() int { if r.Code == nil { return 0 } return *r.Code } func (r *sdkResponseBase) GetMessage() string { if r.Message == nil { return "" } return *r.Message } var _ sdkResponse = (*sdkResponseBase)(nil) ================================================ FILE: pkg/sdk3rd/1panel/v2/api_core_settings_ssl_update.go ================================================ package v2 import ( "context" "net/http" ) type CoreSettingsSSLUpdateRequest struct { Cert string `json:"cert"` Key string `json:"key"` SSLType string `json:"sslType"` SSL string `json:"ssl"` SSLID int64 `json:"sslID"` AutoRestart string `json:"autoRestart"` } type CoreSettingsSSLUpdateResponse struct { sdkResponseBase } func (c *Client) CoreSettingsSSLUpdate(req *CoreSettingsSSLUpdateRequest) (*CoreSettingsSSLUpdateResponse, error) { return c.CoreSettingsSSLUpdateWithContext(context.Background(), req) } func (c *Client) CoreSettingsSSLUpdateWithContext(ctx context.Context, req *CoreSettingsSSLUpdateRequest) (*CoreSettingsSSLUpdateResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/core/settings/ssl/update") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &CoreSettingsSSLUpdateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/1panel/v2/api_website_get.go ================================================ package v2 import ( "context" "fmt" "net/http" ) type WebsiteGetRequest struct { Name string `json:"name"` Type string `json:"type"` Page int32 `json:"page"` PageSize int32 `json:"pageSize"` } type WebsiteGetResponse struct { sdkResponseBase Data *struct { ID int64 `json:"id"` Alias string `json:"alias"` PrimaryDomain string `json:"primaryDomain"` Protocol string `json:"protocol"` Type string `json:"type"` Status string `json:"status"` SitePath string `json:"sitePath"` Remark string `json:"remark"` Domains []*struct { ID int64 `json:"id"` Domain string `json:"domain"` Port int32 `json:"port"` SSL bool `json:"ssl"` UpdatedAt string `json:"updatedAt"` CreatedAt string `json:"createdAt"` } `json:"domains,omitempty"` WebsiteSSLId int64 `json:"webSiteSSLId"` UpdatedAt string `json:"updatedAt"` CreatedAt string `json:"createdAt"` } `json:"data,omitempty"` } func (c *Client) WebsiteGet(websiteId int64) (*WebsiteGetResponse, error) { return c.WebsiteGetWithContext(context.Background(), websiteId) } func (c *Client) WebsiteGetWithContext(ctx context.Context, websiteId int64) (*WebsiteGetResponse, error) { if websiteId == 0 { return nil, fmt.Errorf("sdkerr: unset websiteId") } httpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf("/websites/%d", websiteId)) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &WebsiteGetResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/1panel/v2/api_website_https_get.go ================================================ package v2 import ( "context" "fmt" "net/http" ) type WebsiteHttpsGetResponse struct { sdkResponseBase Data *struct { Enable bool `json:"enable"` HttpConfig string `json:"httpConfig"` WebsiteSSLID int64 `json:"websiteSSLId"` SSLProtocol []string `json:"SSLProtocol"` Algorithm string `json:"algorithm"` Hsts bool `json:"hsts"` Http3 bool `json:"http3"` } `json:"data,omitempty"` } func (c *Client) WebsiteHttpsGet(websiteId int64) (*WebsiteHttpsGetResponse, error) { return c.WebsiteHttpsGetWithContext(context.Background(), websiteId) } func (c *Client) WebsiteHttpsGetWithContext(ctx context.Context, websiteId int64) (*WebsiteHttpsGetResponse, error) { if websiteId == 0 { return nil, fmt.Errorf("sdkerr: unset websiteId") } httpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf("/websites/%d/https", websiteId)) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &WebsiteHttpsGetResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/1panel/v2/api_website_https_post.go ================================================ package v2 import ( "context" "fmt" "net/http" ) type WebsiteHttpsPostRequest struct { WebsiteID int64 `json:"websiteId"` Enable bool `json:"enable"` Type string `json:"type"` WebsiteSSLID int64 `json:"websiteSSLId"` HttpConfig string `json:"httpConfig"` SSLProtocol []string `json:"SSLProtocol"` Algorithm string `json:"algorithm"` Hsts bool `json:"hsts"` Http3 bool `json:"http3"` } type WebsiteHttpsPostResponse struct { sdkResponseBase } func (c *Client) WebsiteHttpsPost(websiteId int64, req *WebsiteHttpsPostRequest) (*WebsiteHttpsPostResponse, error) { return c.WebsiteHttpsPostWithContext(context.Background(), websiteId, req) } func (c *Client) WebsiteHttpsPostWithContext(ctx context.Context, websiteId int64, req *WebsiteHttpsPostRequest) (*WebsiteHttpsPostResponse, error) { if websiteId == 0 { return nil, fmt.Errorf("sdkerr: unset websiteId") } httpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf("/websites/%d/https", websiteId)) if err != nil { return nil, err } else { req.WebsiteID = websiteId httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &WebsiteHttpsPostResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/1panel/v2/api_website_search.go ================================================ package v2 import ( "context" "net/http" ) type WebsiteSearchRequest struct { Name string `json:"name"` Type string `json:"type"` Order string `json:"order"` OrderBy string `json:"orderBy"` Page int32 `json:"page"` PageSize int32 `json:"pageSize"` } type WebsiteSearchResponse struct { sdkResponseBase Data *struct { Items []*struct { ID int64 `json:"id"` Alias string `json:"alias"` PrimaryDomain string `json:"primaryDomain"` Protocol string `json:"protocol"` Type string `json:"type"` Status string `json:"status"` SitePath string `json:"sitePath"` Remark string `json:"remark"` SSLStatus string `json:"sslStatus"` SSLExpireDate string `json:"sslExpireDate"` UpdatedAt string `json:"updatedAt"` CreatedAt string `json:"createdAt"` } `json:"items"` Total int32 `json:"total"` } `json:"data,omitempty"` } func (c *Client) WebsiteSearch(req *WebsiteSearchRequest) (*WebsiteSearchResponse, error) { return c.WebsiteSearchWithContext(context.Background(), req) } func (c *Client) WebsiteSearchWithContext(ctx context.Context, req *WebsiteSearchRequest) (*WebsiteSearchResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/websites/search") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &WebsiteSearchResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/1panel/v2/api_website_ssl_get.go ================================================ package v2 import ( "context" "fmt" "net/http" ) type WebsiteSSLGetResponse struct { sdkResponseBase Data *struct { ID int64 `json:"id"` Provider string `json:"provider"` Description string `json:"description"` PrimaryDomain string `json:"primaryDomain"` Domains string `json:"domains"` Type string `json:"type"` Organization string `json:"organization"` Status string `json:"status"` StartDate string `json:"startDate"` ExpireDate string `json:"expireDate"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` } `json:"data,omitempty"` } func (c *Client) WebsiteSSLGet(sslId int64) (*WebsiteSSLGetResponse, error) { return c.WebsiteSSLGetWithContext(context.Background(), sslId) } func (c *Client) WebsiteSSLGetWithContext(ctx context.Context, sslId int64) (*WebsiteSSLGetResponse, error) { if sslId == 0 { return nil, fmt.Errorf("sdkerr: unset sslId") } httpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf("/websites/ssl/%d", sslId)) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &WebsiteSSLGetResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/1panel/v2/api_website_ssl_search.go ================================================ package v2 import ( "context" "net/http" ) type WebsiteSSLSearchRequest struct { Domain string `json:"domain"` Order string `json:"order"` OrderBy string `json:"orderBy"` Page int32 `json:"page"` PageSize int32 `json:"pageSize"` } type WebsiteSSLSearchResponse struct { sdkResponseBase Data *struct { Items []*struct { ID int64 `json:"id"` PEM string `json:"pem"` PrivateKey string `json:"privateKey"` Domains string `json:"domains"` Description string `json:"description"` Status string `json:"status"` UpdatedAt string `json:"updatedAt"` CreatedAt string `json:"createdAt"` } `json:"items"` Total int32 `json:"total"` } `json:"data,omitempty"` } func (c *Client) WebsiteSSLSearch(req *WebsiteSSLSearchRequest) (*WebsiteSSLSearchResponse, error) { return c.WebsiteSSLSearchWithContext(context.Background(), req) } func (c *Client) WebsiteSSLSearchWithContext(ctx context.Context, req *WebsiteSSLSearchRequest) (*WebsiteSSLSearchResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/websites/ssl/search") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &WebsiteSSLSearchResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/1panel/v2/api_website_ssl_upload.go ================================================ package v2 import ( "context" "net/http" ) type WebsiteSSLUploadRequest struct { SSLID int64 `json:"sslID"` Type string `json:"type"` Certificate string `json:"certificate"` CertificatePath string `json:"certificatePath"` PrivateKey string `json:"privateKey"` PrivateKeyPath string `json:"privateKeyPath"` Description string `json:"description"` } type WebsiteSSLUploadResponse struct { sdkResponseBase } func (c *Client) WebsiteSSLUpload(req *WebsiteSSLUploadRequest) (*WebsiteSSLUploadResponse, error) { return c.WebsiteSSLUploadWithContext(context.Background(), req) } func (c *Client) WebsiteSSLUploadWithContext(ctx context.Context, req *WebsiteSSLUploadRequest) (*WebsiteSSLUploadResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/websites/ssl/upload") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &WebsiteSSLUploadResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/1panel/v2/client.go ================================================ package v2 import ( "crypto/md5" "crypto/tls" "encoding/hex" "encoding/json" "fmt" "net/http" "net/url" "strings" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { client *resty.Client } func NewClient(serverUrl, apiKey string) (*Client, error) { if serverUrl == "" { return nil, fmt.Errorf("sdkerr: unset serverUrl") } if _, err := url.Parse(serverUrl); err != nil { return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err) } if apiKey == "" { return nil, fmt.Errorf("sdkerr: unset apiKey") } client := resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")+"/api/v2"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { timestamp := fmt.Sprintf("%d", time.Now().Unix()) tokenMd5 := md5.Sum([]byte("1panel" + apiKey + timestamp)) tokenMd5Hex := hex.EncodeToString(tokenMd5[:]) req.Header.Set("1Panel-Timestamp", timestamp) req.Header.Set("1Panel-Token", tokenMd5Hex) return nil }) return &Client{client}, nil } func NewClientWithNode(serverUrl, apiKey, node string) (*Client, error) { client, err := NewClient(serverUrl, apiKey) if err != nil { return nil, err } if node == "" { node = "local" } client.client.SetHeader("CurrentNode", node) return client, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) SetTLSConfig(config *tls.Config) *Client { c.client.SetTLSClientConfig(config) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if tcode := res.GetCode(); tcode/100 != 2 { return resp, fmt.Errorf("sdkerr: api error: code='%d', message='%s'", tcode, res.GetMessage()) } } } return resp, nil } ================================================ FILE: pkg/sdk3rd/1panel/v2/types.go ================================================ package v2 type sdkResponse interface { GetCode() int GetMessage() string } type sdkResponseBase struct { Code *int `json:"code,omitempty"` Message *string `json:"message,omitempty"` } func (r *sdkResponseBase) GetCode() int { if r.Code == nil { return 0 } return *r.Code } func (r *sdkResponseBase) GetMessage() string { if r.Message == nil { return "" } return *r.Message } var _ sdkResponse = (*sdkResponseBase)(nil) ================================================ FILE: pkg/sdk3rd/51dnscom/api_domain_list.go ================================================ package dnscom import ( "context" "net/http" ) type DomainListRequest struct { GroupID *string `json:"groupID,omitempty"` Page *int32 `json:"page,omitempty"` PageSize *int32 `json:"pageSize,omitempty"` } type DomainListResponse struct { sdkResponseBase Data *struct { Data []*DomainRecord `json:"data"` Page int32 `json:"page"` PageSize int32 `json:"pageSize"` PageCount int32 `json:"pageCount"` } `json:"data"` } func (c *Client) DomainList(req *DomainListRequest) (*DomainListResponse, error) { return c.DomainListWithContext(context.Background(), req) } func (c *Client) DomainListWithContext(ctx context.Context, req *DomainListRequest) (*DomainListResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/domain/list/", req) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &DomainListResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/51dnscom/api_record_create.go ================================================ package dnscom import ( "context" "net/http" ) type RecordCreateRequest struct { DomainID *string `json:"domainID,omitempty"` ViewID *string `json:"viewID,omitempty"` Type *string `json:"type,omitempty"` Host *string `json:"host,omitempty"` Value *string `json:"value,omitempty"` TTL *int32 `json:"ttl,omitempty"` MX *int32 `json:"mx,omitempty"` Remark *string `json:"remark,omitempty"` } type RecordCreateResponse struct { sdkResponseBase Data *DNSRecord `json:"data"` } func (c *Client) RecordCreate(req *RecordCreateRequest) (*RecordCreateResponse, error) { return c.RecordCreateWithContext(context.Background(), req) } func (c *Client) RecordCreateWithContext(ctx context.Context, req *RecordCreateRequest) (*RecordCreateResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/record/create/", req) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &RecordCreateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/51dnscom/api_record_remove.go ================================================ package dnscom import ( "context" "net/http" ) type RecordRemoveRequest struct { DomainID *string `json:"domainID,omitempty"` RecordID *string `json:"recordID,omitempty"` } type RecordRemoveResponse struct { sdkResponseBase } func (c *Client) RecordRemove(req *RecordRemoveRequest) (*RecordRemoveResponse, error) { return c.RecordRemoveWithContext(context.Background(), req) } func (c *Client) RecordRemoveWithContext(ctx context.Context, req *RecordRemoveRequest) (*RecordRemoveResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/record/remove/", req) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &RecordRemoveResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/51dnscom/client.go ================================================ package dnscom import ( "crypto/md5" "encoding/hex" "encoding/json" "fmt" "sort" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { apiKey string apiSecret string client *resty.Client } func NewClient(apiKey, apiSecret string) (*Client, error) { if apiKey == "" { return nil, fmt.Errorf("sdkerr: unset apiKey") } if apiSecret == "" { return nil, fmt.Errorf("sdkerr: unset apiSecret") } client := resty.New(). SetBaseURL("https://www.51dns.com/api"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent) return &Client{ apiKey: apiKey, apiSecret: apiSecret, client: client, }, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string, params any) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } data := make(map[string]string) if params != nil { temp := make(map[string]any) jsonb, _ := json.Marshal(params) json.Unmarshal(jsonb, &temp) for k, v := range temp { if v == nil { continue } data[k] = fmt.Sprintf("%v", v) } } data["apiKey"] = c.apiKey data["timestamp"] = fmt.Sprintf("%d", time.Now().Unix()) data["hash"] = generateHash(data, c.apiSecret) req := c.client.R() req.Method = method req.URL = path req.SetBody(data) return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetBody` or `req.SetFormData` HERE! USE `newRequest` INSTEAD. // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if tcode := res.GetCode(); tcode != 0 { return resp, fmt.Errorf("sdkerr: api error: code='%d', message='%s'", tcode, res.GetMessage()) } } } return resp, nil } func generateHash(params map[string]string, secert string) string { var keyList []string for k := range params { keyList = append(keyList, k) } sort.Strings(keyList) var hashString string for _, key := range keyList { if hashString == "" { hashString += key + "=" + params[key] } else { hashString += "&" + key + "=" + params[key] } } m := md5.New() m.Write([]byte(hashString + secert)) cipherStr := m.Sum(nil) return hex.EncodeToString(cipherStr) } ================================================ FILE: pkg/sdk3rd/51dnscom/types.go ================================================ package dnscom import ( "encoding/json" ) type sdkResponse interface { GetCode() int GetMessage() string } type sdkResponseBase struct { Code int `json:"code"` Message string `json:"message"` } func (r *sdkResponseBase) GetCode() int { return r.Code } func (r *sdkResponseBase) GetMessage() string { return r.Message } var _ sdkResponse = (*sdkResponseBase)(nil) type DomainRecord struct { GroupID json.Number `json:"groupID"` DomainID json.Number `json:"domainsID"` Domain string `json:"domains"` State int32 `json:"state"` UserLockState int32 `json:"userLock"` AdminLockState int32 `json:"adminLock"` HealthState int32 `json:"healthState"` ViewType string `json:"view_type"` } type DNSRecord struct { DomainID json.Number `json:"domainID"` RecordID json.Number `json:"recordID"` ViewID json.Number `json:"viewID"` Record string `json:"record"` Type string `json:"type"` Host string `json:"host"` Value string `json:"value"` TTL int32 `json:"ttl"` MX int32 `json:"mx"` State int32 `json:"state"` Remark string `json:"remark"` } ================================================ FILE: pkg/sdk3rd/apisix/api_ssl_update.go ================================================ package apisix import ( "context" "fmt" "net/http" "net/url" ) type SslUpdateRequest = SslCertificate type SslUpdateResponse = SslCertificate func (c *Client) SslUpdate(sslId string, req *SslUpdateRequest) (*SslUpdateResponse, error) { return c.SslUpdateWithContext(context.Background(), sslId, req) } func (c *Client) SslUpdateWithContext(ctx context.Context, sslId string, req *SslUpdateRequest) (*SslUpdateResponse, error) { httpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf("/ssls/%s", url.PathEscape(sslId))) if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &SslUpdateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/apisix/client.go ================================================ package apisix import ( "crypto/tls" "encoding/json" "fmt" "net/url" "strings" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { client *resty.Client } func NewClient(serverUrl, apiKey string) (*Client, error) { if serverUrl == "" { return nil, fmt.Errorf("sdkerr: unset serverUrl") } if _, err := url.Parse(serverUrl); err != nil { return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err) } if apiKey == "" { return nil, fmt.Errorf("sdkerr: unset apiKey") } client := resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")+"/apisix/admin"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetHeader("X-Api-Key", apiKey) return &Client{ client: client, }, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) SetTLSConfig(config *tls.Config) *Client { c.client.SetTLSClientConfig(config) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res interface{}) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } } return resp, nil } ================================================ FILE: pkg/sdk3rd/apisix/types.go ================================================ package apisix type SslCertificate struct { ID *string `json:"id,omitempty"` Status *int32 `json:"status,omitempty"` Certificate *string `json:"cert,omitempty"` PrivateKey *string `json:"key,omitempty"` SNIs *[]string `json:"snis,omitempty"` Type *string `json:"type,omitempty"` ValidityStart *int64 `json:"validity_start,omitempty"` ValidityEnd *int64 `json:"validity_end,omitempty"` Labels *map[string]string `json:"labels,omitempty"` } ================================================ FILE: pkg/sdk3rd/azure/env/config.go ================================================ package env import ( "fmt" "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" ) func IsPublicEnv(env string) bool { switch strings.ToLower(env) { case "", "default", "public", "azurecloud": return true default: return false } } func IsUSGovernmentEnv(env string) bool { switch strings.ToLower(env) { case "usgovernment", "government", "azureusgovernment", "azuregovernment": return true default: return false } } func IsChinaEnv(env string) bool { switch strings.ToLower(env) { case "china", "chinacloud", "azurechina", "azurechinacloud": return true default: return false } } func GetCloudEnvConfiguration(env string) (cloud.Configuration, error) { if IsPublicEnv(env) { return cloud.AzurePublic, nil } else if IsUSGovernmentEnv(env) { return cloud.AzureGovernment, nil } else if IsChinaEnv(env) { return cloud.AzureChina, nil } return cloud.Configuration{}, fmt.Errorf("unknown azure cloud environment %s", env) } ================================================ FILE: pkg/sdk3rd/baiducloud/cert/cert.go ================================================ package cert import ( "errors" "fmt" "github.com/baidubce/bce-sdk-go/bce" "github.com/baidubce/bce-sdk-go/http" "github.com/baidubce/bce-sdk-go/services/cert" ) func (c *Client) CreateCert(args *CreateCertArgs) (*CreateCertResult, error) { if args == nil { return nil, errors.New("unset args") } result, err := c.Client.CreateCert(&args.CreateCertArgs) if err != nil { return nil, err } return &CreateCertResult{CreateCertResult: *result}, nil } func (c *Client) ListCerts() (*ListCertResult, error) { result, err := c.Client.ListCerts() if err != nil { return nil, err } return &ListCertResult{ListCertResult: *result}, nil } func (c *Client) ListCertDetail() (*ListCertDetailResult, error) { result, err := c.Client.ListCertDetail() if err != nil { return nil, err } return &ListCertDetailResult{ListCertDetailResult: *result}, nil } func (c *Client) GetCertMeta(id string) (*CertificateMeta, error) { result, err := c.Client.GetCertMeta(id) if err != nil { return nil, err } return &CertificateMeta{CertificateMeta: *result}, nil } func (c *Client) GetCertDetail(id string) (*CertificateDetailMeta, error) { result, err := c.Client.GetCertDetail(id) if err != nil { return nil, err } return &CertificateDetailMeta{CertificateDetailMeta: *result}, nil } func (c *Client) GetCertRawData(id string) (*CertificateRawData, error) { result := &CertificateRawData{} err := bce.NewRequestBuilder(c). WithMethod(http.GET). WithURL(cert.URI_PREFIX + cert.REQUEST_CERT_URL + "/" + id + "/rawData"). WithResult(result). Do() return result, err } func (c *Client) UpdateCertName(id string, args *UpdateCertNameArgs) error { if args == nil { return errors.New("unset args") } err := c.Client.UpdateCertName(id, &args.UpdateCertNameArgs) return err } func (c *Client) UpdateCertData(id string, args *UpdateCertDataArgs) error { if args == nil { return fmt.Errorf("unset args") } err := c.Client.UpdateCertData(id, &args.UpdateCertDataArgs) return err } func (c *Client) DeleteCert(id string) error { err := c.Client.DeleteCert(id) return err } ================================================ FILE: pkg/sdk3rd/baiducloud/cert/client.go ================================================ package cert import ( "github.com/baidubce/bce-sdk-go/services/cert" ) type Client struct { *cert.Client } func NewClient(ak, sk, endPoint string) (*Client, error) { client, err := cert.NewClient(ak, sk, endPoint) if err != nil { return nil, err } return &Client{client}, nil } ================================================ FILE: pkg/sdk3rd/baiducloud/cert/model.go ================================================ package cert import "github.com/baidubce/bce-sdk-go/services/cert" type CreateCertArgs struct { cert.CreateCertArgs } type CreateCertResult struct { cert.CreateCertResult } type UpdateCertNameArgs struct { cert.UpdateCertNameArgs } type CertificateMeta struct { cert.CertificateMeta } type CertificateDetailMeta struct { cert.CertificateDetailMeta } type CertificateRawData struct { CertId string `json:"certId"` CertName string `json:"certName"` CertServerData string `json:"certServerData"` CertPrivateData string `json:"certPrivateKey"` CertLinkData string `json:"certLinkData,omitempty"` CertType int `json:"certType,omitempty"` } type ListCertResult struct { cert.ListCertResult } type ListCertDetailResult struct { cert.ListCertDetailResult } type UpdateCertDataArgs struct { cert.UpdateCertDataArgs } type CertInServiceMeta struct { cert.CertInServiceMeta } ================================================ FILE: pkg/sdk3rd/baishan/api_get_domain_config.go ================================================ package baishan import ( "context" "net/http" ) type GetDomainConfigRequest struct { Domains *string `json:"domains,omitempty" url:"domains,omitempty"` Config *[]string `json:"config,omitempty" url:"config,omitempty"` } type GetDomainConfigResponse struct { sdkResponseBase Data []*struct { Domain string `json:"domain"` Config *DomainConfig `json:"config"` } `json:"data,omitempty"` } func (c *Client) GetDomainConfig(req *GetDomainConfigRequest) (*GetDomainConfigResponse, error) { return c.GetDomainConfigWithContext(context.Background(), req) } func (c *Client) GetDomainConfigWithContext(ctx context.Context, req *GetDomainConfigRequest) (*GetDomainConfigResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/v2/domain/config") if err != nil { return nil, err } else { if req.Domains != nil { httpreq.SetQueryParam("domains", *req.Domains) } if req.Config != nil { for _, config := range *req.Config { httpreq.QueryParam.Add("config[]", config) } } httpreq.SetContext(ctx) } result := &GetDomainConfigResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/baishan/api_get_domain_list.go ================================================ package baishan import ( "context" "encoding/json" "net/http" qs "github.com/google/go-querystring/query" ) type GetDomainListRequest struct { PageNumber *int32 `json:"page_number,omitempty" url:"page_number,omitempty"` PageSize *int32 `json:"page_size,omitempty" url:"page_size,omitempty"` DomainStatus *string `json:"domain_status,omitempty" url:"domain_status,omitempty"` } type GetDomainListResponse struct { sdkResponseBase Data []*struct { List []*DomainRecord `json:"list"` PageNumber json.Number `json:"page_number"` PageSize json.Number `json:"page_size"` TotalNumber json.Number `json:"total_number"` } `json:"data,omitempty"` } func (c *Client) GetDomainList(req *GetDomainListRequest) (*GetDomainListResponse, error) { return c.GetDomainListWithContext(context.Background(), req) } func (c *Client) GetDomainListWithContext(ctx context.Context, req *GetDomainListRequest) (*GetDomainListResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/v2/domain/list") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &GetDomainListResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/baishan/api_set_domain_config.go ================================================ package baishan import ( "context" "net/http" ) type SetDomainConfigRequest struct { Domains *string `json:"domains,omitempty"` Config *DomainConfig `json:"config,omitempty"` } type SetDomainConfigResponse struct { sdkResponseBase Data *struct { Config *DomainConfig `json:"config"` } `json:"data,omitempty"` } func (c *Client) SetDomainConfig(req *SetDomainConfigRequest) (*SetDomainConfigResponse, error) { return c.SetDomainConfigWithContext(context.Background(), req) } func (c *Client) SetDomainConfigWithContext(ctx context.Context, req *SetDomainConfigRequest) (*SetDomainConfigResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/v2/domain/config") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &SetDomainConfigResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/baishan/api_upload_domain_certificate.go ================================================ package baishan import ( "context" "net/http" ) type UploadDomainCertificateRequest struct { CertificateId *string `json:"cert_id,omitempty"` Certificate *string `json:"certificate,omitempty"` Key *string `json:"key,omitempty"` Name *string `json:"name,omitempty"` } type UploadDomainCertificateResponse struct { sdkResponseBase Data *DomainCertificate `json:"data,omitempty"` } func (c *Client) UploadDomainCertificate(req *UploadDomainCertificateRequest) (*UploadDomainCertificateResponse, error) { return c.UploadDomainCertificateWithContext(context.Background(), req) } func (c *Client) UploadDomainCertificateWithContext(ctx context.Context, req *UploadDomainCertificateRequest) (*UploadDomainCertificateResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/v2/domain/certificate") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &UploadDomainCertificateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/baishan/client.go ================================================ package baishan import ( "encoding/json" "fmt" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { client *resty.Client } func NewClient(apiToken string) (*Client, error) { if apiToken == "" { return nil, fmt.Errorf("sdkerr: unset apiToken") } client := resty.New(). SetBaseURL("https://cdn.api.baishan.com"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetQueryParam("token", apiToken) return &Client{client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if tcode := res.GetCode(); tcode != 0 { return resp, fmt.Errorf("sdkerr: code='%d', message='%s'", tcode, res.GetMessage()) } } } return resp, nil } ================================================ FILE: pkg/sdk3rd/baishan/types.go ================================================ package baishan import ( "encoding/json" ) type sdkResponse interface { GetCode() int GetMessage() string } type sdkResponseBase struct { Code *int `json:"code,omitempty"` Message *string `json:"message,omitempty"` } func (r *sdkResponseBase) GetCode() int { if r.Code == nil { return 0 } return *r.Code } func (r *sdkResponseBase) GetMessage() string { if r.Message == nil { return "" } return *r.Message } var _ sdkResponse = (*sdkResponseBase)(nil) type DomainRecord struct { Id string `json:"id"` Domain string `json:"domain"` Type string `json:"type"` Status string `json:"status"` Cname string `json:"cname"` Area string `json:"area"` CreateTime string `json:"create_time"` UpdateTime string `json:"update_time"` } type DomainCertificate struct { CertId json.Number `json:"cert_id"` Name string `json:"name"` CertStartTime string `json:"cert_start_time"` CertExpireTime string `json:"cert_expire_time"` } type DomainConfig struct { Https *DomainConfigHttps `json:"https"` } type DomainConfigHttps struct { CertId json.Number `json:"cert_id"` ForceHttps *string `json:"force_https,omitempty"` EnableHttp2 *string `json:"http2,omitempty"` EnableOcsp *string `json:"ocsp,omitempty"` } ================================================ FILE: pkg/sdk3rd/btpanel/api_config_save_panel_ssl.go ================================================ package btpanel import ( "context" "net/http" ) type ConfigSavePanelSSLRequest struct { PrivateKey string `json:"privateKey"` Certificate string `json:"certPem"` } type ConfigSavePanelSSLResponse struct { sdkResponseBase } func (c *Client) ConfigSavePanelSSL(req *ConfigSavePanelSSLRequest) (*ConfigSavePanelSSLResponse, error) { return c.ConfigSavePanelSSLWithContext(context.Background(), req) } func (c *Client) ConfigSavePanelSSLWithContext(ctx context.Context, req *ConfigSavePanelSSLRequest) (*ConfigSavePanelSSLResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/config?action=SavePanelSSL", req) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &ConfigSavePanelSSLResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/btpanel/api_mod_proxy_com_set_ssl.go ================================================ package btpanel import ( "context" "net/http" ) type ModProxyComSetSSLRequest struct { SiteName string `json:"site_name"` PrivateKey string `json:"key"` Certificate string `json:"csr"` } type ModProxyComSetSSLResponse struct { sdkResponseBase } func (c *Client) ModProxyComSetSSL(req *ModProxyComSetSSLRequest) (*ModProxyComSetSSLResponse, error) { return c.ModProxyComSetSSLWithContext(context.Background(), req) } func (c *Client) ModProxyComSetSSLWithContext(ctx context.Context, req *ModProxyComSetSSLRequest) (*ModProxyComSetSSLResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/mod/proxy/com/set_ssl", req) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &ModProxyComSetSSLResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/btpanel/api_site_set_ssl.go ================================================ package btpanel import ( "context" "net/http" ) type SiteSetSSLRequest struct { Type string `json:"type"` SiteName string `json:"siteName"` PrivateKey string `json:"key"` Certificate string `json:"csr"` } type SiteSetSSLResponse struct { sdkResponseBase } func (c *Client) SiteSetSSL(req *SiteSetSSLRequest) (*SiteSetSSLResponse, error) { return c.SiteSetSSLWithContext(context.Background(), req) } func (c *Client) SiteSetSSLWithContext(ctx context.Context, req *SiteSetSSLRequest) (*SiteSetSSLResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/site?action=SetSSL", req) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &SiteSetSSLResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/btpanel/api_ssl_cert_save_cert.go ================================================ package btpanel import ( "context" "net/http" ) type SSLCertSaveCertRequest struct { PrivateKey string `json:"key"` Certificate string `json:"csr"` } type SSLCertSaveCertResponse struct { sdkResponseBase SSLHash string `json:"ssl_hash"` } func (c *Client) SSLCertSaveCert(req *SSLCertSaveCertRequest) (*SSLCertSaveCertResponse, error) { return c.SSLCertSaveCertWithContext(context.Background(), req) } func (c *Client) SSLCertSaveCertWithContext(ctx context.Context, req *SSLCertSaveCertRequest) (*SSLCertSaveCertResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/ssl/cert/save_cert", req) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &SSLCertSaveCertResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/btpanel/api_ssl_set_batch_cert_to_site.go ================================================ package btpanel import ( "context" "net/http" ) type SSLSetBatchCertToSiteRequest struct { BatchInfo []*SSLSetBatchCertToSiteRequestBatchInfo `json:"BatchInfo"` } type SSLSetBatchCertToSiteRequestBatchInfo struct { SSLHash string `json:"ssl_hash"` SiteName string `json:"siteName"` CertName string `json:"certName"` } type SSLSetBatchCertToSiteResponse struct { sdkResponseBase TotalCount int32 `json:"total"` SuccessCount int32 `json:"success"` FailedCount int32 `json:"faild"` } func (c *Client) SSLSetBatchCertToSite(req *SSLSetBatchCertToSiteRequest) (*SSLSetBatchCertToSiteResponse, error) { return c.SSLSetBatchCertToSiteWithContext(context.Background(), req) } func (c *Client) SSLSetBatchCertToSiteWithContext(ctx context.Context, req *SSLSetBatchCertToSiteRequest) (*SSLSetBatchCertToSiteResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/ssl?action=SetBatchCertToSite", req) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &SSLSetBatchCertToSiteResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/btpanel/api_system_service_admin.go ================================================ package btpanel import ( "context" "net/http" ) type SystemServiceAdminRequest struct { Name string `json:"name"` Type string `json:"type"` } type SystemServiceAdminResponse struct { sdkResponseBase } func (c *Client) SystemServiceAdmin(req *SystemServiceAdminRequest) (*SystemServiceAdminResponse, error) { return c.SystemServiceAdminWithContext(context.Background(), req) } func (c *Client) SystemServiceAdminWithContext(ctx context.Context, req *SystemServiceAdminRequest) (*SystemServiceAdminResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/system?action=ServiceAdmin", req) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &SystemServiceAdminResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/btpanel/client.go ================================================ package btpanel import ( "crypto/md5" "crypto/tls" "encoding/hex" "encoding/json" "fmt" "net/url" "reflect" "strings" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { apiKey string client *resty.Client } func NewClient(serverUrl, apiKey string) (*Client, error) { if serverUrl == "" { return nil, fmt.Errorf("sdkerr: unset serverUrl") } if _, err := url.Parse(serverUrl); err != nil { return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err) } if apiKey == "" { return nil, fmt.Errorf("sdkerr: unset apiKey") } client := resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/x-www-form-urlencoded"). SetHeader("User-Agent", app.AppUserAgent) return &Client{ apiKey: apiKey, client: client, }, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) SetTLSConfig(config *tls.Config) *Client { c.client.SetTLSClientConfig(config) return c } func (c *Client) newRequest(method string, path string, params any) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } data := make(map[string]string) if params != nil { temp := make(map[string]any) jsonb, _ := json.Marshal(params) json.Unmarshal(jsonb, &temp) for k, v := range temp { if v == nil { continue } switch reflect.Indirect(reflect.ValueOf(v)).Kind() { case reflect.String: data[k] = v.(string) case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64: data[k] = fmt.Sprintf("%v", v) default: if t, ok := v.(time.Time); ok { data[k] = t.Format(time.RFC3339) } else { jsonb, _ := json.Marshal(v) data[k] = string(jsonb) } } } } timestamp := time.Now().Unix() data["request_time"] = fmt.Sprintf("%d", timestamp) data["request_token"] = generateSignature(fmt.Sprintf("%d", timestamp), c.apiKey) req := c.client.R() req.Method = method req.URL = path req.SetFormData(data) return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetBody` or `req.SetFormData` HERE! USE `newRequest` INSTEAD. // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if tstatus := res.GetStatus(); tstatus != nil && !*tstatus { if res.GetMessage() == nil { return resp, fmt.Errorf("sdkerr: api error: unknown error") } else { return resp, fmt.Errorf("sdkerr: api error: message='%s'", *res.GetMessage()) } } } } return resp, nil } func generateSignature(timestamp string, apiKey string) string { keyMd5 := md5.Sum([]byte(apiKey)) keyMd5Hex := strings.ToLower(hex.EncodeToString(keyMd5[:])) signMd5 := md5.Sum([]byte(timestamp + keyMd5Hex)) signMd5Hex := strings.ToLower(hex.EncodeToString(signMd5[:])) return signMd5Hex } ================================================ FILE: pkg/sdk3rd/btpanel/types.go ================================================ package btpanel type sdkResponse interface { GetStatus() *bool GetMessage() *string } type sdkResponseBase struct { Status *bool `json:"status,omitempty"` Message *string `json:"msg,omitempty"` } func (r *sdkResponseBase) GetStatus() *bool { return r.Status } func (r *sdkResponseBase) GetMessage() *string { return r.Message } ================================================ FILE: pkg/sdk3rd/btpanelgo/api_config_set_panel_ssl.go ================================================ package btpanel import ( "context" "net/http" ) type ConfigSetPanelSSLRequest struct { SSLStatus *int32 `json:"ssl_status,omitempty"` SSLKey *string `json:"ssl_key,omitempty"` SSLPem *string `json:"ssl_pem,omitempty"` } type ConfigSetPanelSSLResponse struct { sdkResponseBase } func (c *Client) ConfigSetPanelSSL(req *ConfigSetPanelSSLRequest) (*ConfigSetPanelSSLResponse, error) { return c.ConfigSetPanelSSLWithContext(context.Background(), req) } func (c *Client) ConfigSetPanelSSLWithContext(ctx context.Context, req *ConfigSetPanelSSLRequest) (*ConfigSetPanelSSLResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/config/set_panel_ssl", req, false) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &ConfigSetPanelSSLResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/btpanelgo/api_datalist_get_data_list.go ================================================ package btpanel import ( "context" "net/http" ) type DatalistGetDataListRequest struct { Table *string `json:"table,omitempty"` SearchType *string `json:"search_type,omitempty"` SearchString *string `json:"search,omitempty"` Page *int32 `json:"p,omitempty"` Limit *int32 `json:"limit,omitempty"` Order *string `json:"order,omitempty"` Type *int32 `json:"type,omitempty"` } type DatalistGetDataListResponse struct { sdkResponseBase Data []*SiteData `json:"data,omitempty"` Page *PageData `json:"page,omitempty"` } func (c *Client) DatalistGetDataList(req *DatalistGetDataListRequest) (*DatalistGetDataListResponse, error) { return c.DatalistGetDataListWithContext(context.Background(), req) } func (c *Client) DatalistGetDataListWithContext(ctx context.Context, req *DatalistGetDataListRequest) (*DatalistGetDataListResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/datalist/get_data_list", req, false) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &DatalistGetDataListResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/btpanelgo/api_files_upload.go ================================================ package btpanel import ( "context" "net/http" ) type FilesUploadRequest struct { Path *string `json:"path,omitempty"` Name *string `json:"filename,omitempty"` Start *int32 `json:"start,omitempty"` Size *int32 `json:"size,omitempty"` Blob []byte `json:"-" form:"blob"` Force *bool `json:"force,omitempty"` } type FilesUploadResponse struct { sdkResponseBase } func (c *Client) FilesUpload(req *FilesUploadRequest) (*FilesUploadResponse, error) { return c.FilesUploadWithContext(context.Background(), req) } func (c *Client) FilesUploadWithContext(ctx context.Context, req *FilesUploadRequest) (*FilesUploadResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/files/upload", req, true) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &FilesUploadResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/btpanelgo/api_panel_get_config.go ================================================ package btpanel import ( "context" "net/http" ) type PanelGetConfigRequest struct{} type PanelGetConfigResponse struct { sdkResponseBase Paths *struct { Panel string `json:"panel,omitempty"` Soft string `json:"soft,omitempty"` } `json:"paths,omitempty"` Site *struct { WebServer string `json:"webserver,omitempty"` SitesPath string `json:"sites_path,omitempty"` BackupPath string `json:"backup_path,omitempty"` } `json:"site,omitempty"` } func (c *Client) PanelGetConfig(req *PanelGetConfigRequest) (*PanelGetConfigResponse, error) { return c.PanelGetConfigWithContext(context.Background(), req) } func (c *Client) PanelGetConfigWithContext(ctx context.Context, req *PanelGetConfigRequest) (*PanelGetConfigResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/panel/get_config", req, false) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &PanelGetConfigResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/btpanelgo/api_site_get_project_list.go ================================================ package btpanel import ( "context" "net/http" ) type SiteGetProjectListRequest struct { SearchType *string `json:"search_type,omitempty"` SearchString *string `json:"search,omitempty"` Page *int32 `json:"p,omitempty"` Limit *int32 `json:"limit,omitempty"` Order *string `json:"order,omitempty"` } type SiteGetProjectListResponse struct { sdkResponseBase Data []*SiteData `json:"data,omitempty"` Page *PageData `json:"page,omitempty"` } func (c *Client) SiteGetProjectList(req *SiteGetProjectListRequest) (*SiteGetProjectListResponse, error) { return c.SiteGetProjectListWithContext(context.Background(), req) } func (c *Client) SiteGetProjectListWithContext(ctx context.Context, req *SiteGetProjectListRequest) (*SiteGetProjectListResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/site/get_project_list", req, false) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &SiteGetProjectListResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/btpanelgo/api_site_set_site_pfx_ssl.go ================================================ package btpanel import ( "context" "net/http" ) type SiteSetSitePFXSSLRequest struct { SiteId *int32 `json:"siteid,omitempty"` PFX *string `json:"pfx,omitempty"` Password *string `json:"password,omitempty"` } type SiteSetSitePFXSSLResponse struct { sdkResponseBase } func (c *Client) SiteSetSitePFXSSL(req *SiteSetSitePFXSSLRequest) (*SiteSetSitePFXSSLResponse, error) { return c.SiteSetSitePFXSSLWithContext(context.Background(), req) } func (c *Client) SiteSetSitePFXSSLWithContext(ctx context.Context, req *SiteSetSitePFXSSLRequest) (*SiteSetSitePFXSSLResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/site/set_site_pfx_ssl", req, false) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &SiteSetSitePFXSSLResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/btpanelgo/api_site_set_site_ssl.go ================================================ package btpanel import ( "context" "net/http" ) type SiteSetSiteSSLRequest struct { SiteId *int32 `json:"siteid,omitempty"` Status *bool `json:"status,omitempty"` Key *string `json:"key,omitempty"` Cert *string `json:"cert,omitempty"` } type SiteSetSiteSSLResponse struct { sdkResponseBase } func (c *Client) SiteSetSiteSSL(req *SiteSetSiteSSLRequest) (*SiteSetSiteSSLResponse, error) { return c.SiteSetSiteSSLWithContext(context.Background(), req) } func (c *Client) SiteSetSiteSSLWithContext(ctx context.Context, req *SiteSetSiteSSLRequest) (*SiteSetSiteSSLResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/site/set_site_ssl", req, false) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &SiteSetSiteSSLResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/btpanelgo/client.go ================================================ package btpanel import ( "bytes" "crypto/md5" "crypto/tls" "encoding/hex" "encoding/json" "fmt" "net/url" "reflect" "strings" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { apiKey string client *resty.Client } func NewClient(serverUrl, apiKey string) (*Client, error) { if serverUrl == "" { return nil, fmt.Errorf("sdkerr: unset serverUrl") } if _, err := url.Parse(serverUrl); err != nil { return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err) } if apiKey == "" { return nil, fmt.Errorf("sdkerr: unset apiKey") } client := resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/x-www-form-urlencoded"). SetHeader("User-Agent", app.AppUserAgent) return &Client{ apiKey: apiKey, client: client, }, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) SetTLSConfig(config *tls.Config) *Client { c.client.SetTLSClientConfig(config) return c } func (c *Client) newRequest(method string, path string, params any, multipart bool) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } data := make(map[string]string) if params != nil { temp := make(map[string]any) jsonb, _ := json.Marshal(params) json.Unmarshal(jsonb, &temp) for k, v := range temp { if v == nil { continue } switch reflect.Indirect(reflect.ValueOf(v)).Kind() { case reflect.String: data[k] = v.(string) case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64: data[k] = fmt.Sprintf("%v", v) default: if t, ok := v.(time.Time); ok { data[k] = t.Format(time.RFC3339) } else { jsonb, _ := json.Marshal(v) data[k] = string(jsonb) } } } } timestamp := time.Now().Unix() data["request_time"] = fmt.Sprintf("%d", timestamp) data["request_token"] = generateSignature(fmt.Sprintf("%d", timestamp), c.apiKey) req := c.client.R() req.Method = method req.URL = path if multipart { req.SetMultipartFormData(data) if params != nil { vparams := reflect.ValueOf(params) if vparams.Kind() == reflect.Ptr { vparams = vparams.Elem() } if vparams.Kind() == reflect.Struct { vparamsTyp := vparams.Type() for i := 0; i < vparams.NumField(); i++ { field := vparamsTyp.Field(i) fieldVal := vparams.Field(i) if fieldVal.Kind() == reflect.Ptr && fieldVal.IsNil() { continue } formTag := field.Tag.Get("form") if formTag == "" { continue } switch v := fieldVal.Interface().(type) { case []byte: req.SetMultipartField(formTag, formTag, "", bytes.NewReader(v)) default: panic("unreachable") } } } else { panic("unreachable") } } } else { req.SetFormData(data) } return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetBody` or `req.SetFormData` HERE! USE `newRequest` INSTEAD. // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if tstatus := res.GetStatus(); tstatus != nil { var errored bool var bstatus bool if err := json.Unmarshal(tstatus, &bstatus); err == nil { errored = !bstatus } var istatus int if err := json.Unmarshal(tstatus, &istatus); err == nil { errored = istatus != 0 } if errored { if res.GetMessage() == nil { return resp, fmt.Errorf("sdkerr: api error: unknown error") } else { return resp, fmt.Errorf("sdkerr: api error: message='%s'", *res.GetMessage()) } } } } } return resp, nil } func generateSignature(timestamp string, apiKey string) string { keyMd5 := md5.Sum([]byte(apiKey)) keyMd5Hex := strings.ToLower(hex.EncodeToString(keyMd5[:])) signMd5 := md5.Sum([]byte(timestamp + keyMd5Hex)) signMd5Hex := strings.ToLower(hex.EncodeToString(signMd5[:])) return signMd5Hex } ================================================ FILE: pkg/sdk3rd/btpanelgo/types.go ================================================ package btpanel import ( "encoding/json" ) type sdkResponse interface { GetStatus() json.RawMessage GetMessage() *string } type sdkResponseBase struct { Status json.RawMessage `json:"status,omitempty"` Code *int `json:"code,omitempty"` Message *string `json:"msg,omitempty"` } func (r *sdkResponseBase) GetStatus() json.RawMessage { return r.Status } func (r *sdkResponseBase) GetMessage() *string { return r.Message } type SiteData struct { Id int32 `json:"id"` ProjectType string `json:"project_type"` Name string `json:"name"` Note string `json:"ps"` Status string `json:"status"` SSLInfo []*struct { Name string `json:"name"` Status bool `json:"status"` } `json:"ssl_info"` AddTime string `json:"addtime"` } type PageData struct { Page int32 `json:"page"` Limit int32 `json:"limit"` Total int32 `json:"total"` Start int32 `json:"start"` End int32 `json:"end"` MaxPage int32 `json:"maxPage"` } ================================================ FILE: pkg/sdk3rd/btwaf/api_config_set_cert.go ================================================ package btwaf import ( "context" "net/http" ) type ConfigSetCertRequest struct { CertContent *string `json:"certContent,omitempty"` KeyContent *string `json:"keyContent,omitempty"` } type ConfigSetCertResponse struct { sdkResponseBase } func (c *Client) ConfigSetCert(req *ConfigSetCertRequest) (*ConfigSetCertResponse, error) { return c.ConfigSetCertWithContext(context.Background(), req) } func (c *Client) ConfigSetCertWithContext(ctx context.Context, req *ConfigSetCertRequest) (*ConfigSetCertResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/config/set_cert") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &ConfigSetCertResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/btwaf/api_get_site_list.go ================================================ package btwaf import ( "context" "net/http" ) type GetSiteListRequest struct { SiteName *string `json:"site_name,omitempty"` Page *int32 `json:"p,omitempty"` PageSize *int32 `json:"p_size,omitempty"` } type GetSiteListResponse struct { sdkResponseBase Result *struct { List []*SiteRecord `json:"list"` Total int32 `json:"total"` } `json:"res,omitempty"` } func (c *Client) GetSiteList(req *GetSiteListRequest) (*GetSiteListResponse, error) { return c.GetSiteListWithContext(context.Background(), req) } func (c *Client) GetSiteListWithContext(ctx context.Context, req *GetSiteListRequest) (*GetSiteListResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/wafmastersite/get_site_list") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &GetSiteListResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/btwaf/api_modify_site.go ================================================ package btwaf import ( "context" "net/http" ) type ModifySiteRequest struct { SiteId *string `json:"site_id,omitempty"` Type *string `json:"types,omitempty"` Server *SiteServerInfoMod `json:"server,omitempty"` } type ModifySiteResponse struct { sdkResponseBase } func (c *Client) ModifySite(req *ModifySiteRequest) (*ModifySiteResponse, error) { return c.ModifySiteWithContext(context.Background(), req) } func (c *Client) ModifySiteWithContext(ctx context.Context, req *ModifySiteRequest) (*ModifySiteResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/wafmastersite/modify_site") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &ModifySiteResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/btwaf/client.go ================================================ package btwaf import ( "crypto/md5" "crypto/tls" "encoding/hex" "encoding/json" "fmt" "net/http" "net/url" "strings" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { client *resty.Client } func NewClient(serverUrl, apiKey string) (*Client, error) { if serverUrl == "" { return nil, fmt.Errorf("sdkerr: unset serverUrl") } if _, err := url.Parse(serverUrl); err != nil { return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err) } if apiKey == "" { return nil, fmt.Errorf("sdkerr: unset apiKey") } client := resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")+"/api"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { timestamp := fmt.Sprintf("%d", time.Now().Unix()) keyMd5 := md5.Sum([]byte(apiKey)) keyMd5Hex := strings.ToLower(hex.EncodeToString(keyMd5[:])) signMd5 := md5.Sum([]byte(timestamp + keyMd5Hex)) signMd5Hex := strings.ToLower(hex.EncodeToString(signMd5[:])) req.Header.Set("waf_request_time", timestamp) req.Header.Set("waf_request_token", signMd5Hex) return nil }) return &Client{client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) SetTLSConfig(config *tls.Config) *Client { c.client.SetTLSClientConfig(config) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if code := res.GetCode(); code != 0 { return resp, fmt.Errorf("sdkerr: api error: code='%d'", code) } } } return resp, nil } ================================================ FILE: pkg/sdk3rd/btwaf/types.go ================================================ package btwaf type sdkResponse interface { GetCode() int } type sdkResponseBase struct { Code *int `json:"code,omitempty"` } func (r *sdkResponseBase) GetCode() int { if r.Code == nil { return 0 } return *r.Code } var _ sdkResponse = (*sdkResponseBase)(nil) type SiteRecord struct { SiteId string `json:"site_id"` SiteName string `json:"site_name"` Type string `json:"types"` Status int32 `json:"status"` ServerNames []string `json:"server_name"` CreateTime int64 `json:"create_time"` UpdateTime int64 `json:"update_time"` } // type SiteServerInfo struct { // ListenSSLPorts *[]int32 `json:"listen_ssl_port,omitempty"` // SSL *SiteServerSSLInfo `json:"ssl,omitempty"` // } type SiteServerInfoMod struct { ListenSSLPorts *[]string `json:"listen_ssl_port,omitempty"` SSL *SiteServerSSLInfo `json:"ssl,omitempty"` } type SiteServerSSLInfo struct { IsSSL *int32 `json:"is_ssl,omitempty"` FullChain *string `json:"full_chain,omitempty"` PrivateKey *string `json:"private_key,omitempty"` } ================================================ FILE: pkg/sdk3rd/bunny/api_add_custom_certificate.go ================================================ package bunny import ( "context" "fmt" "net/http" "net/url" ) type AddCustomCertificateRequest struct { Hostname string `json:"Hostname"` Certificate string `json:"Certificate"` CertificateKey string `json:"CertificateKey"` } func (c *Client) AddCustomCertificate(pullZoneId string, req *AddCustomCertificateRequest) error { return c.AddCustomCertificateWithContext(context.Background(), pullZoneId, req) } func (c *Client) AddCustomCertificateWithContext(ctx context.Context, pullZoneId string, req *AddCustomCertificateRequest) error { if pullZoneId == "" { return fmt.Errorf("sdkerr: unset pullZoneId") } httpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf("/pullzone/%s/addCertificate", url.PathEscape(pullZoneId))) if err != nil { return err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } if _, err := c.doRequest(httpreq); err != nil { return err } return nil } ================================================ FILE: pkg/sdk3rd/bunny/client.go ================================================ package bunny import ( "fmt" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { client *resty.Client } func NewClient(apiToken string) (*Client, error) { if apiToken == "" { return nil, fmt.Errorf("sdkerr: unset apiToken") } client := resty.New(). SetBaseURL("https://api.bunny.net"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetHeader("AccessKey", apiToken) return &Client{client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } ================================================ FILE: pkg/sdk3rd/cachefly/api_create_certificate.go ================================================ package cachefly import ( "context" "net/http" ) type CreateCertificateRequest struct { Certificate *string `json:"certificate,omitempty"` CertificateKey *string `json:"certificateKey,omitempty"` Password *string `json:"password,omitempty"` } type CreateCertificateResponse struct { sdkResponseBase Id string `json:"_id"` SubjectCommonName string `json:"subjectCommonName"` SubjectNames []string `json:"subjectNames"` Expired bool `json:"expired"` Expiring bool `json:"expiring"` InUse bool `json:"inUse"` Managed bool `json:"managed"` Services []string `json:"services"` Domains []string `json:"domains"` NotBefore string `json:"notBefore"` NotAfter string `json:"notAfter"` CreatedAt string `json:"createdAt"` } func (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) { return c.CreateCertificateWithContext(context.Background(), req) } func (c *Client) CreateCertificateWithContext(ctx context.Context, req *CreateCertificateRequest) (*CreateCertificateResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/certificates") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &CreateCertificateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/cachefly/client.go ================================================ package cachefly import ( "encoding/json" "fmt" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { client *resty.Client } func NewClient(apiToken string) (*Client, error) { if apiToken == "" { return nil, fmt.Errorf("sdkerr: unset apiToken") } client := resty.New(). SetBaseURL("https://api.cachefly.com/api/2.5"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetHeader("X-CF-Authorization", "Bearer "+apiToken) return &Client{client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } } return resp, nil } ================================================ FILE: pkg/sdk3rd/cachefly/types.go ================================================ package cachefly type sdkResponse interface { GetMessage() string } type sdkResponseBase struct { Message *string `json:"message,omitempty"` } func (r *sdkResponseBase) GetMessage() string { if r.Message == nil { return "" } return *r.Message } var _ sdkResponse = (*sdkResponseBase)(nil) ================================================ FILE: pkg/sdk3rd/cdnfly/api_create_cert.go ================================================ package cdnfly import ( "context" "net/http" ) type CreateCertRequest struct { Name *string `json:"name,omitempty"` Description *string `json:"des,omitempty"` Type *string `json:"type,omitempty"` Cert *string `json:"cert,omitempty"` Key *string `json:"key,omitempty"` } type CreateCertResponse struct { sdkResponseBase Data string `json:"data"` } func (c *Client) CreateCert(req *CreateCertRequest) (*CreateCertResponse, error) { return c.CreateCertWithContext(context.Background(), req) } func (c *Client) CreateCertWithContext(ctx context.Context, req *CreateCertRequest) (*CreateCertResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/certs") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &CreateCertResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/cdnfly/api_get_site.go ================================================ package cdnfly import ( "context" "fmt" "net/http" "net/url" ) type GetSiteResponse struct { sdkResponseBase Data *struct { Id int64 `json:"id"` Name string `json:"name"` Domain string `json:"domain"` HttpsListen string `json:"https_listen"` } `json:"data,omitempty"` } func (c *Client) GetSite(siteId string) (*GetSiteResponse, error) { return c.GetSiteWithContext(context.Background(), siteId) } func (c *Client) GetSiteWithContext(ctx context.Context, siteId string) (*GetSiteResponse, error) { if siteId == "" { return nil, fmt.Errorf("sdkerr: unset siteId") } httpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf("/sites/%s", url.PathEscape(siteId))) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &GetSiteResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/cdnfly/api_update_cert.go ================================================ package cdnfly import ( "context" "fmt" "net/http" "net/url" ) type UpdateCertRequest struct { Name *string `json:"name,omitempty"` Description *string `json:"des,omitempty"` Type *string `json:"type,omitempty"` Cert *string `json:"cert,omitempty"` Key *string `json:"key,omitempty"` Enable *bool `json:"enable,omitempty"` } type UpdateCertResponse struct { sdkResponseBase } func (c *Client) UpdateCert(certId string, req *UpdateCertRequest) (*UpdateCertResponse, error) { return c.UpdateCertWithContext(context.Background(), certId, req) } func (c *Client) UpdateCertWithContext(ctx context.Context, certId string, req *UpdateCertRequest) (*UpdateCertResponse, error) { if certId == "" { return nil, fmt.Errorf("sdkerr: unset certId") } httpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf("/certs/%s", url.PathEscape(certId))) if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &UpdateCertResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/cdnfly/api_update_site.go ================================================ package cdnfly import ( "context" "fmt" "net/http" "net/url" ) type UpdateSiteRequest struct { HttpsListen *string `json:"https_listen,omitempty"` Enable *bool `json:"enable,omitempty"` } type UpdateSiteResponse struct { sdkResponseBase } func (c *Client) UpdateSite(siteId string, req *UpdateSiteRequest) (*UpdateSiteResponse, error) { return c.UpdateSiteWithContext(context.Background(), siteId, req) } func (c *Client) UpdateSiteWithContext(ctx context.Context, siteId string, req *UpdateSiteRequest) (*UpdateSiteResponse, error) { if siteId == "" { return nil, fmt.Errorf("sdkerr: unset siteId") } httpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf("/sites/%s", url.PathEscape(siteId))) if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &UpdateSiteResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/cdnfly/client.go ================================================ package cdnfly import ( "crypto/tls" "encoding/json" "fmt" "net/url" "strings" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { client *resty.Client } func NewClient(serverUrl, apiKey, apiSecret string) (*Client, error) { if serverUrl == "" { return nil, fmt.Errorf("sdkerr: unset serverUrl") } if _, err := url.Parse(serverUrl); err != nil { return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err) } if apiKey == "" { return nil, fmt.Errorf("sdkerr: unset apiKey") } if apiSecret == "" { return nil, fmt.Errorf("sdkerr: unset apiSecret") } client := resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")+"/v1"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetHeader("API-Key", apiKey). SetHeader("API-Secret", apiSecret) return &Client{client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) SetTLSConfig(config *tls.Config) *Client { c.client.SetTLSClientConfig(config) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if tcode := res.GetCode(); tcode != "" && tcode != "0" { return resp, fmt.Errorf("sdkerr: code='%s', message='%s'", tcode, res.GetMessage()) } } } return resp, nil } ================================================ FILE: pkg/sdk3rd/cdnfly/types.go ================================================ package cdnfly import ( "bytes" "encoding/json" "strconv" ) type sdkResponse interface { GetCode() string GetMessage() string } type sdkResponseBase struct { Code json.RawMessage `json:"code"` Message string `json:"msg"` } func (r *sdkResponseBase) GetCode() string { if r.Code == nil { return "" } decoder := json.NewDecoder(bytes.NewReader(r.Code)) token, err := decoder.Token() if err != nil { return "" } switch t := token.(type) { case string: return t case float64: return strconv.FormatFloat(t, 'f', -1, 64) case json.Number: return t.String() default: return "" } } func (r *sdkResponseBase) GetMessage() string { return r.Message } var _ sdkResponse = (*sdkResponseBase)(nil) ================================================ FILE: pkg/sdk3rd/cpanel/api_ssl_install_ssl.go ================================================ package baishan import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type SSLInstallSSLRequest struct { Domain *string `url:"domain,omitempty"` Cert *string `url:"cert,omitempty"` Key *string `url:"key,omitempty"` CABundle *string `url:"cabundle,omitempty"` } type SSLInstallSSLResponse struct { sdkResponseBase Data *struct { User string `json:"user"` Domain string `json:"domain"` ExtraCertificateDomains []string `json:"extra_certificate_domains,omitempty"` WarningDomains []string `json:"warning_domains,omitempty"` WorkingDomains []string `json:"working_domains,omitempty"` CertId string `json:"cert_id"` KeyId string `json:"key_id"` } `json:"data,omitempty"` } func (c *Client) SSLInstallSSL(req *SSLInstallSSLRequest) (*SSLInstallSSLResponse, error) { return c.SSLInstallSSLWithContext(context.Background(), req) } func (c *Client) SSLInstallSSLWithContext(ctx context.Context, req *SSLInstallSSLRequest) (*SSLInstallSSLResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/SSL/install_ssl") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &SSLInstallSSLResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/cpanel/client.go ================================================ package baishan import ( "crypto/tls" "encoding/json" "fmt" "net/url" "strings" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { client *resty.Client } func NewClient(serverUrl string, username, apiToken string) (*Client, error) { if serverUrl == "" { return nil, fmt.Errorf("sdkerr: unset serverUrl") } if _, err := url.Parse(serverUrl); err != nil { return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err) } if username == "" { return nil, fmt.Errorf("sdkerr: unset username") } if apiToken == "" { return nil, fmt.Errorf("sdkerr: unset apiToken") } client := resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")+"/execute"). SetHeader("Accept", "application/json"). SetHeader("Authorization", fmt.Sprintf("cpanel %s:%s", username, apiToken)). SetHeader("User-Agent", app.AppUserAgent) return &Client{client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) SetTLSConfig(config *tls.Config) *Client { c.client.SetTLSClientConfig(config) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if tstatus := res.GetStatus(); tstatus == 0 { return resp, fmt.Errorf("sdkerr: status='%d', messages='%s', warnings='%s', errors='%s'", tstatus, strings.Join(res.GetMessages(), ", "), strings.Join(res.GetWarnings(), ", "), strings.Join(res.GetErrors(), ", ")) } } } return resp, nil } ================================================ FILE: pkg/sdk3rd/cpanel/types.go ================================================ package baishan type sdkResponse interface { GetStatus() int GetMessages() []string GetWarnings() []string GetErrors() []string } type sdkResponseBase struct { Metadata struct { Transformed int `json:"transformed,omitempty"` } `json:"metadata"` Status int `json:"status,omitempty"` Messages []string `json:"messages,omitempty"` Warnings []string `json:"warnings,omitempty"` Errors []string `json:"errors,omitempty"` } func (r *sdkResponseBase) GetStatus() int { return r.Status } func (r *sdkResponseBase) GetMessages() []string { return r.Messages } func (r *sdkResponseBase) GetWarnings() []string { return r.Warnings } func (r *sdkResponseBase) GetErrors() []string { return r.Errors } var _ sdkResponse = (*sdkResponseBase)(nil) ================================================ FILE: pkg/sdk3rd/ctyun/ao/api_create_cert.go ================================================ package ao import ( "context" "net/http" ) type CreateCertRequest struct { Name *string `json:"name,omitempty"` Certs *string `json:"certs,omitempty"` Key *string `json:"key,omitempty"` } type CreateCertResponse struct { sdkResponseBase ReturnObj *struct { Id int64 `json:"id"` } `json:"returnObj,omitempty"` } func (c *Client) CreateCert(req *CreateCertRequest) (*CreateCertResponse, error) { return c.CreateCertWithContext(context.Background(), req) } func (c *Client) CreateCertWithContext(ctx context.Context, req *CreateCertRequest) (*CreateCertResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/ctapi/v1/accessone/cert/create") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &CreateCertResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/ao/api_get_domain_config.go ================================================ package ao import ( "context" "net/http" ) type GetDomainConfigRequest struct { Domain *string `json:"domain,omitempty"` ProductCode *string `json:"product_code,omitempty"` } type GetDomainConfigResponse struct { sdkResponseBase ReturnObj *struct { Domain string `json:"domain"` ProductCode string `json:"product_code"` Status int32 `json:"status"` AreaScope int32 `json:"area_scope"` Cname string `json:"cname"` Origin []*DomainOriginConfigWithWeight `json:"origin,omitempty"` HttpsStatus string `json:"https_status"` HttpsBasic *DomainHttpsBasicConfig `json:"https_basic,omitempty"` CertName string `json:"cert_name"` } `json:"returnObj,omitempty"` } func (c *Client) GetDomainConfig(req *GetDomainConfigRequest) (*GetDomainConfigResponse, error) { return c.GetDomainConfigWithContext(context.Background(), req) } func (c *Client) GetDomainConfigWithContext(ctx context.Context, req *GetDomainConfigRequest) (*GetDomainConfigResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/ctapi/v1/accessone/domain/config") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &GetDomainConfigResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/ao/api_list_certs.go ================================================ package ao import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type ListCertsRequest struct { Page *int32 `json:"page,omitempty" url:"page,omitempty"` PerPage *int32 `json:"per_page,omitempty" url:"per_page,omitempty"` UsageMode *int32 `json:"usage_mode,omitempty" url:"usage_mode,omitempty"` } type ListCertsResponse struct { sdkResponseBase ReturnObj *struct { Results []*CertRecord `json:"result,omitempty"` Page int32 `json:"page,omitempty"` PerPage int32 `json:"per_page,omitempty"` TotalPage int32 `json:"total_page,omitempty"` TotalRecords int32 `json:"total_records,omitempty"` } `json:"returnObj,omitempty"` } func (c *Client) ListCerts(req *ListCertsRequest) (*ListCertsResponse, error) { return c.ListCertsWithContext(context.Background(), req) } func (c *Client) ListCertsWithContext(ctx context.Context, req *ListCertsRequest) (*ListCertsResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/ctapi/v1/accessone/cert/list") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &ListCertsResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/ao/api_modify_domain_config.go ================================================ package ao import ( "context" "net/http" ) type ModifyDomainConfigRequest struct { Domain *string `json:"domain,omitempty"` ProductCode *string `json:"product_code,omitempty"` Origin []*DomainOriginConfig `json:"origin,omitempty"` HttpsStatus *string `json:"https_status,omitempty"` HttpsBasic *DomainHttpsBasicConfig `json:"https_basic,omitempty"` CertName *string `json:"cert_name,omitempty"` } type ModifyDomainConfigResponse struct { sdkResponseBase } func (c *Client) ModifyDomainConfig(req *ModifyDomainConfigRequest) (*ModifyDomainConfigResponse, error) { return c.ModifyDomainConfigWithContext(context.Background(), req) } func (c *Client) ModifyDomainConfigWithContext(ctx context.Context, req *ModifyDomainConfigRequest) (*ModifyDomainConfigResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/ctapi/v1/scdn/domain/modify_config") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &ModifyDomainConfigResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/ao/api_query_cert.go ================================================ package ao import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type QueryCertRequest struct { Id *int64 `json:"id,omitempty" url:"id,omitempty"` Name *string `json:"name,omitempty" url:"name,omitempty"` UsageMode *int32 `json:"usage_mode,omitempty" url:"usage_mode,omitempty"` } type QueryCertResponse struct { sdkResponseBase ReturnObj *struct { Result *CertDetail `json:"result,omitempty"` } `json:"returnObj,omitempty"` } func (c *Client) QueryCert(req *QueryCertRequest) (*QueryCertResponse, error) { return c.QueryCertWithContext(context.Background(), req) } func (c *Client) QueryCertWithContext(ctx context.Context, req *QueryCertRequest) (*QueryCertResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/ctapi/v1/accessone/cert/query") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &QueryCertResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/ao/api_query_domains.go ================================================ package ao import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type QueryDomainsRequest struct { Page *int32 `json:"page,omitempty" url:"page,omitempty"` PageSize *int32 `json:"page_size,omitempty" url:"page_size,omitempty"` Domain *string `json:"domain,omitempty" url:"domain,omitempty"` ProductCode *string `json:"product_code,omitempty" url:"product_code,omitempty"` Status *int32 `json:"status,omitempty" url:"status,omitempty"` AreaScope *int32 `json:"area_scope,omitempty" url:"area_scope,omitempty"` } type QueryDomainsResponse struct { sdkResponseBase ReturnObj *struct { Results []*DomainRecord `json:"result,omitempty"` Page int32 `json:"page,omitempty"` PageSize int32 `json:"page_size,omitempty"` PageCount int32 `json:"page_count,omitempty"` Total int32 `json:"total,omitempty"` } `json:"returnObj,omitempty"` } func (c *Client) QueryDomains(req *QueryDomainsRequest) (*QueryDomainsResponse, error) { return c.QueryDomainsWithContext(context.Background(), req) } func (c *Client) QueryDomainsWithContext(ctx context.Context, req *QueryDomainsRequest) (*QueryDomainsResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/ctapi/v2/domain/query") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &QueryDomainsResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/ao/client.go ================================================ package ao import ( "fmt" "time" "github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/openapi" "github.com/go-resty/resty/v2" ) const endpoint = "https://accessone-global.ctapi.ctyun.cn" type Client struct { client *openapi.Client } func NewClient(accessKeyId, secretAccessKey string) (*Client, error) { client, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey) if err != nil { return nil, err } return &Client{client: client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { return c.client.NewRequest(method, path) } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { return c.client.DoRequest(req) } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { resp, err := c.client.DoRequestWithResult(req, res) if err == nil { if tcode := res.GetStatusCode(); tcode != "" && tcode != "100000" { return resp, fmt.Errorf("sdkerr: api error: code='%s', message='%s', errorCode='%s', errorMessage='%s'", tcode, res.GetMessage(), res.GetMessage(), res.GetErrorMessage()) } } return resp, err } ================================================ FILE: pkg/sdk3rd/ctyun/ao/types.go ================================================ package ao import ( "bytes" "encoding/json" "strconv" ) type sdkResponse interface { GetStatusCode() string GetMessage() string GetError() string GetErrorMessage() string } type sdkResponseBase struct { StatusCode json.RawMessage `json:"statusCode,omitempty"` Message *string `json:"message,omitempty"` Error *string `json:"error,omitempty"` ErrorMessage *string `json:"errorMessage,omitempty"` RequestId *string `json:"requestId,omitempty"` } func (r *sdkResponseBase) GetStatusCode() string { if r.StatusCode == nil { return "" } decoder := json.NewDecoder(bytes.NewReader(r.StatusCode)) token, err := decoder.Token() if err != nil { return "" } switch t := token.(type) { case string: return t case float64: return strconv.FormatFloat(t, 'f', -1, 64) case json.Number: return t.String() default: return "" } } func (r *sdkResponseBase) GetMessage() string { if r.Message == nil { return "" } return *r.Message } func (r *sdkResponseBase) GetError() string { if r.Error == nil { return "" } return *r.Error } func (r *sdkResponseBase) GetErrorMessage() string { if r.ErrorMessage == nil { return "" } return *r.ErrorMessage } var _ sdkResponse = (*sdkResponseBase)(nil) type CertRecord struct { Id int64 `json:"id"` Name string `json:"name"` CN string `json:"cn"` SANs []string `json:"sans"` UsageMode int32 `json:"usage_mode"` State int32 `json:"state"` ExpiresTime int64 `json:"expires"` IssueTime int64 `json:"issue"` Issuer string `json:"issuer"` CreatedTime int64 `json:"created"` } type CertDetail struct { CertRecord Certs string `json:"certs"` Key string `json:"key"` } type DomainRecord struct { Domain string `json:"domain"` Cname string `json:"cname"` ProductCode string `json:"product_code"` ProductName string `json:"product_name"` Status int32 `json:"status"` AreaScope int32 `json:"area_scope"` } type DomainOriginConfig struct { Origin string `json:"origin"` Role string `json:"role"` Weight string `json:"weight"` } type DomainOriginConfigWithWeight struct { Origin string `json:"origin"` Role string `json:"role"` Weight int32 `json:"weight"` } type DomainHttpsBasicConfig struct { HttpsForce string `json:"https_force,omitempty"` ForceStatus string `json:"force_status,omitempty"` } ================================================ FILE: pkg/sdk3rd/ctyun/cdn/api_create_cert.go ================================================ package cdn import ( "context" "net/http" ) type CreateCertRequest struct { Name *string `json:"name,omitempty"` Certs *string `json:"certs,omitempty"` Key *string `json:"key,omitempty"` } type CreateCertResponse struct { sdkResponseBase ReturnObj *struct { Id int64 `json:"id"` } `json:"returnObj,omitempty"` } func (c *Client) CreateCert(req *CreateCertRequest) (*CreateCertResponse, error) { return c.CreateCertWithContext(context.Background(), req) } func (c *Client) CreateCertWithContext(ctx context.Context, req *CreateCertRequest) (*CreateCertResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/v1/cert/creat-cert") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &CreateCertResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/cdn/api_query_cert_detail.go ================================================ package cdn import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type QueryCertDetailRequest struct { Id *int64 `json:"id,omitempty" url:"id,omitempty"` Name *string `json:"name,omitempty" url:"name,omitempty"` UsageMode *int32 `json:"usage_mode,omitempty" url:"usage_mode,omitempty"` } type QueryCertDetailResponse struct { sdkResponseBase ReturnObj *struct { Result *CertDetail `json:"result,omitempty"` } `json:"returnObj,omitempty"` } func (c *Client) QueryCertDetail(req *QueryCertDetailRequest) (*QueryCertDetailResponse, error) { return c.QueryCertDetailWithContext(context.Background(), req) } func (c *Client) QueryCertDetailWithContext(ctx context.Context, req *QueryCertDetailRequest) (*QueryCertDetailResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/v1/cert/query-cert-detail") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &QueryCertDetailResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/cdn/api_query_cert_list.go ================================================ package cdn import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type QueryCertListRequest struct { Page *int32 `json:"page,omitempty" url:"page,omitempty"` PerPage *int32 `json:"per_page,omitempty" url:"per_page,omitempty"` UsageMode *int32 `json:"usage_mode,omitempty" url:"usage_mode,omitempty"` } type QueryCertListResponse struct { sdkResponseBase ReturnObj *struct { Results []*CertRecord `json:"result,omitempty"` Page int32 `json:"page,omitempty"` PerPage int32 `json:"per_page,omitempty"` TotalPage int32 `json:"total_page,omitempty"` TotalRecords int32 `json:"total_records,omitempty"` } `json:"returnObj,omitempty"` } func (c *Client) QueryCertList(req *QueryCertListRequest) (*QueryCertListResponse, error) { return c.QueryCertListWithContext(context.Background(), req) } func (c *Client) QueryCertListWithContext(ctx context.Context, req *QueryCertListRequest) (*QueryCertListResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/v1/cert/query-cert-list") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &QueryCertListResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/cdn/api_query_domain_detail.go ================================================ package cdn import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type QueryDomainDetailRequest struct { Domain *string `json:"domain,omitempty" url:"domain,omitempty"` ProductCode *string `json:"product_code,omitempty" url:"product_code,omitempty"` FunctionNames *string `json:"function_names,omitempty" url:"function_names,omitempty"` } type QueryDomainDetailResponse struct { sdkResponseBase ReturnObj *DomainDetail `json:"returnObj,omitempty"` } func (c *Client) QueryDomainDetail(req *QueryDomainDetailRequest) (*QueryDomainDetailResponse, error) { return c.QueryDomainDetailWithContext(context.Background(), req) } func (c *Client) QueryDomainDetailWithContext(ctx context.Context, req *QueryDomainDetailRequest) (*QueryDomainDetailResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/v1/domain/query-domain-detail") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &QueryDomainDetailResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/cdn/api_query_domain_list.go ================================================ package cdn import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type QueryDomainListRequest struct { Page *int32 `json:"page,omitempty" url:"page,omitempty"` PageSize *int32 `json:"page_size,omitempty" url:"page_size,omitempty"` Domain *string `json:"domain,omitempty" url:"domain,omitempty"` ProductCode *string `json:"product_code,omitempty" url:"product_code,omitempty"` Status *int32 `json:"status,omitempty" url:"status,omitempty"` AreaScope *int32 `json:"area_scope,omitempty" url:"area_scope,omitempty"` } type QueryDomainListResponse struct { sdkResponseBase ReturnObj *struct { Results []*DomainRecord `json:"result,omitempty"` Page int32 `json:"page,omitempty"` PageSize int32 `json:"page_size,omitempty"` PageCount int32 `json:"page_count,omitempty"` Total int32 `json:"total,omitempty"` } `json:"returnObj,omitempty"` } func (c *Client) QueryDomainList(req *QueryDomainListRequest) (*QueryDomainListResponse, error) { return c.QueryDomainListWithContext(context.Background(), req) } func (c *Client) QueryDomainListWithContext(ctx context.Context, req *QueryDomainListRequest) (*QueryDomainListResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/v1/domain/query-domain-list") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &QueryDomainListResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/cdn/api_update_domain.go ================================================ package cdn import ( "context" "net/http" ) type UpdateDomainRequest struct { Domain *string `json:"domain,omitempty"` HttpsStatus *string `json:"https_status,omitempty"` CertName *string `json:"cert_name,omitempty"` } type UpdateDomainResponse struct { sdkResponseBase } func (c *Client) UpdateDomain(req *UpdateDomainRequest) (*UpdateDomainResponse, error) { return c.UpdateDomainWithContext(context.Background(), req) } func (c *Client) UpdateDomainWithContext(ctx context.Context, req *UpdateDomainRequest) (*UpdateDomainResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/v1/domain/update-domain") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &UpdateDomainResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/cdn/client.go ================================================ package cdn import ( "fmt" "time" "github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/openapi" "github.com/go-resty/resty/v2" ) const endpoint = "https://ctcdn-global.ctapi.ctyun.cn" type Client struct { client *openapi.Client } func NewClient(accessKeyId, secretAccessKey string) (*Client, error) { client, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey) if err != nil { return nil, err } return &Client{client: client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { return c.client.NewRequest(method, path) } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { return c.client.DoRequest(req) } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { resp, err := c.client.DoRequestWithResult(req, res) if err == nil { if tcode := res.GetStatusCode(); tcode != "" && tcode != "100000" { return resp, fmt.Errorf("sdkerr: api error: code='%s', message='%s', errorCode='%s', errorMessage='%s'", tcode, res.GetMessage(), res.GetMessage(), res.GetErrorMessage()) } } return resp, err } ================================================ FILE: pkg/sdk3rd/ctyun/cdn/types.go ================================================ package cdn import ( "bytes" "encoding/json" "strconv" ) type sdkResponse interface { GetStatusCode() string GetMessage() string GetError() string GetErrorMessage() string } type sdkResponseBase struct { StatusCode json.RawMessage `json:"statusCode,omitempty"` Message *string `json:"message,omitempty"` Error *string `json:"error,omitempty"` ErrorMessage *string `json:"errorMessage,omitempty"` RequestId *string `json:"requestId,omitempty"` } func (r *sdkResponseBase) GetStatusCode() string { if r.StatusCode == nil { return "" } decoder := json.NewDecoder(bytes.NewReader(r.StatusCode)) token, err := decoder.Token() if err != nil { return "" } switch t := token.(type) { case string: return t case float64: return strconv.FormatFloat(t, 'f', -1, 64) case json.Number: return t.String() default: return "" } } func (r *sdkResponseBase) GetMessage() string { if r.Message == nil { return "" } return *r.Message } func (r *sdkResponseBase) GetError() string { if r.Error == nil { return "" } return *r.Error } func (r *sdkResponseBase) GetErrorMessage() string { if r.ErrorMessage == nil { return "" } return *r.ErrorMessage } var _ sdkResponse = (*sdkResponseBase)(nil) type DomainRecord struct { Domain string `json:"domain"` Cname string `json:"cname"` ProductCode string `json:"product_code"` ProductName string `json:"product_name"` AreaScope int32 `json:"area_scope"` Status int32 `json:"status"` } type DomainDetail struct { DomainRecord HttpsStatus string `json:"https_status"` HttpsBasic *DomainHttpsBasicConfig `json:"https_basic,omitempty"` CertName string `json:"cert_name"` Ssl string `json:"ssl"` SslStapling string `json:"ssl_stapling"` } type DomainHttpsBasicConfig struct { HttpsForce string `json:"https_force"` HttpForce string `json:"http_force"` ForceStatus string `json:"force_status"` OriginProtocol string `json:"origin_protocol"` } type CertRecord struct { Id int64 `json:"id"` Name string `json:"name"` CN string `json:"cn"` SANs []string `json:"sans"` UsageMode int32 `json:"usage_mode"` State int32 `json:"state"` ExpiresTime int64 `json:"expires"` IssueTime int64 `json:"issue"` Issuer string `json:"issuer"` CreatedTime int64 `json:"created"` } type CertDetail struct { CertRecord Certs string `json:"certs"` Key string `json:"key"` } ================================================ FILE: pkg/sdk3rd/ctyun/cms/api_get_certificate_list.go ================================================ package cms import ( "context" "net/http" ) type GetCertificateListRequest struct { Status *string `json:"status,omitempty"` Keyword *string `json:"keyword,omitempty"` PageNum *int32 `json:"pageNum,omitempty"` PageSize *int32 `json:"pageSize,omitempty"` Origin *string `json:"origin,omitempty"` } type GetCertificateListResponse struct { sdkResponseBase ReturnObj *struct { List []*CertificateRecord `json:"list,omitempty"` TotalSize int32 `json:"totalSize,omitempty"` } `json:"returnObj,omitempty"` } func (c *Client) GetCertificateList(req *GetCertificateListRequest) (*GetCertificateListResponse, error) { return c.GetCertificateListWithContext(context.Background(), req) } func (c *Client) GetCertificateListWithContext(ctx context.Context, req *GetCertificateListRequest) (*GetCertificateListResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/v1/certificate/list") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &GetCertificateListResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/cms/api_upload_certificate.go ================================================ package cms import ( "context" "net/http" ) type UploadCertificateRequest struct { Name *string `json:"name,omitempty"` Certificate *string `json:"certificate,omitempty"` CertificateChain *string `json:"certificateChain,omitempty"` PrivateKey *string `json:"privateKey,omitempty"` EncryptionStandard *string `json:"encryptionStandard,omitempty"` EncCertificate *string `json:"encCertificate,omitempty"` EncPrivateKey *string `json:"encPrivateKey,omitempty"` } type UploadCertificateResponse struct { sdkResponseBase } func (c *Client) UploadCertificate(req *UploadCertificateRequest) (*UploadCertificateResponse, error) { return c.UploadCertificateWithContext(context.Background(), req) } func (c *Client) UploadCertificateWithContext(ctx context.Context, req *UploadCertificateRequest) (*UploadCertificateResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/v1/certificate/upload") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &UploadCertificateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/cms/client.go ================================================ package cms import ( "fmt" "time" "github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/openapi" "github.com/go-resty/resty/v2" ) const endpoint = "https://ccms-global.ctapi.ctyun.cn" type Client struct { client *openapi.Client } func NewClient(accessKeyId, secretAccessKey string) (*Client, error) { client, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey) if err != nil { return nil, err } return &Client{client: client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { return c.client.NewRequest(method, path) } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { return c.client.DoRequest(req) } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { resp, err := c.client.DoRequestWithResult(req, res) if err == nil { statusCode := res.GetStatusCode() errorCode := res.GetError() if (statusCode != "" && statusCode != "200") || errorCode != "" { return resp, fmt.Errorf("sdkerr: api error: code='%s', message='%s', errorCode='%s', errorMessage='%s'", statusCode, res.GetMessage(), res.GetMessage(), res.GetErrorMessage()) } } return resp, err } ================================================ FILE: pkg/sdk3rd/ctyun/cms/types.go ================================================ package cms import ( "bytes" "encoding/json" "strconv" ) type sdkResponse interface { GetStatusCode() string GetMessage() string GetError() string GetErrorMessage() string } type sdkResponseBase struct { StatusCode json.RawMessage `json:"statusCode,omitempty"` Message *string `json:"message,omitempty"` Error *string `json:"error,omitempty"` ErrorMessage *string `json:"errorMessage,omitempty"` RequestId *string `json:"requestId,omitempty"` } func (r *sdkResponseBase) GetStatusCode() string { if r.StatusCode == nil { return "" } decoder := json.NewDecoder(bytes.NewReader(r.StatusCode)) token, err := decoder.Token() if err != nil { return "" } switch t := token.(type) { case string: return t case float64: return strconv.FormatFloat(t, 'f', -1, 64) case json.Number: return t.String() default: return "" } } func (r *sdkResponseBase) GetMessage() string { if r.Message == nil { return "" } return *r.Message } func (r *sdkResponseBase) GetError() string { if r.Error == nil { return "" } return *r.Error } func (r *sdkResponseBase) GetErrorMessage() string { if r.ErrorMessage == nil { return "" } return *r.ErrorMessage } var _ sdkResponse = (*sdkResponseBase)(nil) type CertificateRecord struct { Id string `json:"id"` Origin string `json:"origin"` Type string `json:"type"` ResourceId string `json:"resourceId"` ResourceType string `json:"resourceType"` CertificateId string `json:"certificateId"` CertificateMode string `json:"certificateMode"` Name string `json:"name"` Status string `json:"status"` DetailStatus string `json:"detailStatus"` ManagedStatus string `json:"managedStatus"` Fingerprint string `json:"fingerprint"` IssueTime string `json:"issueTime"` ExpireTime string `json:"expireTime"` DomainType string `json:"domainType"` DomainName string `json:"domainName"` EncryptionStandard string `json:"encryptionStandard"` EncryptionAlgorithm string `json:"encryptionAlgorithm"` CreateTime string `json:"createTime"` UpdateTime string `json:"updateTime"` } ================================================ FILE: pkg/sdk3rd/ctyun/dns/api_add_record.go ================================================ package dns import ( "context" "net/http" ) type AddRecordRequest struct { Domain *string `json:"domain,omitempty"` Host *string `json:"host,omitempty"` Type *string `json:"type,omitempty"` LineCode *string `json:"lineCode,omitempty"` Value *string `json:"value,omitempty"` TTL *int32 `json:"ttl,omitempty"` State *int32 `json:"state,omitempty"` Remark *string `json:"remark"` } type AddRecordResponse struct { sdkResponseBase ReturnObj *struct { RecordId int32 `json:"recordId"` } `json:"returnObj,omitempty"` } func (c *Client) AddRecord(req *AddRecordRequest) (*AddRecordResponse, error) { return c.AddRecordWithContext(context.Background(), req) } func (c *Client) AddRecordWithContext(ctx context.Context, req *AddRecordRequest) (*AddRecordResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/v2/addRecord") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &AddRecordResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/dns/api_delete_record.go ================================================ package dns import ( "context" "net/http" ) type DeleteRecordRequest struct { RecordId *int32 `json:"recordId,omitempty"` } type DeleteRecordResponse struct { sdkResponseBase } func (c *Client) DeleteRecord(req *DeleteRecordRequest) (*DeleteRecordResponse, error) { return c.DeleteRecordWithContext(context.Background(), req) } func (c *Client) DeleteRecordWithContext(ctx context.Context, req *DeleteRecordRequest) (*DeleteRecordResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/v2/deleteRecord") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &DeleteRecordResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/dns/api_query_record_list.go ================================================ package dns import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type QueryRecordListRequest struct { Domain *string `json:"domain,omitempty" url:"domain,omitempty"` Host *string `json:"host,omitempty" url:"host,omitempty"` Type *string `json:"type,omitempty" url:"type,omitempty"` LineCode *string `json:"lineCode,omitempty" url:"lineCode,omitempty"` Value *string `json:"value,omitempty" url:"value,omitempty"` State *int32 `json:"state,omitempty" url:"state,omitempty"` } type QueryRecordListResponse struct { sdkResponseBase ReturnObj *struct { Records []*DnsRecord `json:"records,omitempty"` } `json:"returnObj,omitempty"` } func (c *Client) QueryRecordList(req *QueryRecordListRequest) (*QueryRecordListResponse, error) { return c.QueryRecordListWithContext(context.Background(), req) } func (c *Client) QueryRecordListWithContext(ctx context.Context, req *QueryRecordListRequest) (*QueryRecordListResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/v2/queryRecordList") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &QueryRecordListResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/dns/api_update_record.go ================================================ package dns import ( "context" "net/http" ) type UpdateRecordRequest struct { RecordId *int32 `json:"recordId,omitempty"` Domain *string `json:"domain,omitempty"` Host *string `json:"host,omitempty"` Type *string `json:"type,omitempty"` LineCode *string `json:"lineCode,omitempty"` Value *string `json:"value,omitempty"` TTL *int32 `json:"ttl,omitempty"` State *int32 `json:"state,omitempty"` Remark *string `json:"remark"` } type UpdateRecordResponse struct { sdkResponseBase ReturnObj *struct { RecordId int32 `json:"recordId"` } `json:"returnObj,omitempty"` } func (c *Client) UpdateRecord(req *UpdateRecordRequest) (*UpdateRecordResponse, error) { return c.UpdateRecordWithContext(context.Background(), req) } func (c *Client) UpdateRecordWithContext(ctx context.Context, req *UpdateRecordRequest) (*UpdateRecordResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/v2/updateRecord") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &UpdateRecordResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/dns/client.go ================================================ package dns import ( "fmt" "time" "github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/openapi" "github.com/go-resty/resty/v2" ) const endpoint = "https://smartdns-global.ctapi.ctyun.cn" type Client struct { client *openapi.Client } func NewClient(accessKeyId, secretAccessKey string) (*Client, error) { client, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey) if err != nil { return nil, err } return &Client{client: client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { return c.client.NewRequest(method, path) } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { return c.client.DoRequest(req) } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { resp, err := c.client.DoRequestWithResult(req, res) if err == nil { statusCode := res.GetStatusCode() errorCode := res.GetError() if (statusCode != "" && statusCode != "200") || errorCode != "" { return resp, fmt.Errorf("sdkerr: api error: code='%s', message='%s', errorCode='%s', errorMessage='%s'", statusCode, res.GetMessage(), res.GetMessage(), res.GetErrorMessage()) } } return resp, err } ================================================ FILE: pkg/sdk3rd/ctyun/dns/types.go ================================================ package dns import ( "bytes" "encoding/json" "strconv" ) type sdkResponse interface { GetStatusCode() string GetMessage() string GetError() string GetErrorMessage() string } type sdkResponseBase struct { StatusCode json.RawMessage `json:"statusCode,omitempty"` Message *string `json:"message,omitempty"` Error *string `json:"error,omitempty"` ErrorMessage *string `json:"errorMessage,omitempty"` RequestId *string `json:"requestId,omitempty"` } func (r *sdkResponseBase) GetStatusCode() string { if r.StatusCode == nil { return "" } decoder := json.NewDecoder(bytes.NewReader(r.StatusCode)) token, err := decoder.Token() if err != nil { return "" } switch t := token.(type) { case string: return t case float64: return strconv.FormatFloat(t, 'f', -1, 64) case json.Number: return t.String() default: return "" } } func (r *sdkResponseBase) GetMessage() string { if r.Message == nil { return "" } return *r.Message } func (r *sdkResponseBase) GetError() string { if r.Error == nil { return "" } return *r.Error } func (r *sdkResponseBase) GetErrorMessage() string { if r.ErrorMessage == nil { return "" } return *r.ErrorMessage } var _ sdkResponse = (*sdkResponseBase)(nil) type DnsRecord struct { RecordId int32 `json:"recordId"` Host string `json:"host"` Type string `json:"type"` LineCode string `json:"lineCode"` Value string `json:"value"` TTL int32 `json:"ttl"` State int32 `json:"state"` Remark string `json:"remark"` } ================================================ FILE: pkg/sdk3rd/ctyun/elb/api_create_certificate.go ================================================ package elb import ( "context" "net/http" ) type CreateCertificateRequest struct { ClientToken *string `json:"clientToken,omitempty"` RegionID *string `json:"regionID,omitempty"` Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` Type *string `json:"type,omitempty"` Certificate *string `json:"certificate,omitempty"` PrivateKey *string `json:"privateKey,omitempty"` } type CreateCertificateResponse struct { sdkResponseBase ReturnObj *struct { ID string `json:"id"` } `json:"returnObj,omitempty"` } func (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) { return c.CreateCertificateWithContext(context.Background(), req) } func (c *Client) CreateCertificateWithContext(ctx context.Context, req *CreateCertificateRequest) (*CreateCertificateResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/v4/elb/create-certificate") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &CreateCertificateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/elb/api_list_certificates.go ================================================ package elb import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type ListCertificatesRequest struct { ClientToken *string `json:"clientToken,omitempty" url:"clientToken,omitempty"` RegionID *string `json:"regionID,omitempty" url:"regionID,omitempty"` IDs *string `json:"IDs,omitempty" url:"IDs,omitempty"` Name *string `json:"name,omitempty" url:"name,omitempty"` Type *string `json:"type,omitempty" url:"type,omitempty"` } type ListCertificatesResponse struct { sdkResponseBase ReturnObj []*CertificateRecord `json:"returnObj,omitempty"` } func (c *Client) ListCertificates(req *ListCertificatesRequest) (*ListCertificatesResponse, error) { return c.ListCertificatesWithContext(context.Background(), req) } func (c *Client) ListCertificatesWithContext(ctx context.Context, req *ListCertificatesRequest) (*ListCertificatesResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/v4/elb/list-certificate") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &ListCertificatesResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/elb/api_list_listeners.go ================================================ package elb import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type ListListenersRequest struct { ClientToken *string `json:"clientToken,omitempty" url:"clientToken,omitempty"` RegionID *string `json:"regionID,omitempty" url:"regionID,omitempty"` ProjectID *string `json:"projectID,omitempty" url:"projectID,omitempty"` IDs *string `json:"IDs,omitempty" url:"IDs,omitempty"` Name *string `json:"name,omitempty" url:"name,omitempty"` LoadBalancerID *string `json:"loadBalancerID,omitempty" url:"loadBalancerID,omitempty"` AccessControlID *string `json:"accessControlID,omitempty" url:"accessControlID,omitempty"` } type ListListenersResponse struct { sdkResponseBase ReturnObj []*ListenerRecord `json:"returnObj,omitempty"` } func (c *Client) ListListeners(req *ListListenersRequest) (*ListListenersResponse, error) { return c.ListListenersWithContext(context.Background(), req) } func (c *Client) ListListenersWithContext(ctx context.Context, req *ListListenersRequest) (*ListListenersResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/v4/elb/list-listener") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &ListListenersResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/elb/api_show_listener.go ================================================ package elb import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type ShowListenerRequest struct { ClientToken *string `json:"clientToken,omitempty" url:"clientToken,omitempty"` RegionID *string `json:"regionID,omitempty" url:"regionID,omitempty"` ListenerID *string `json:"listenerID,omitempty" url:"listenerID,omitempty"` } type ShowListenerResponse struct { sdkResponseBase ReturnObj []*ListenerRecord `json:"returnObj,omitempty"` } func (c *Client) ShowListener(req *ShowListenerRequest) (*ShowListenerResponse, error) { return c.ShowListenerWithContext(context.Background(), req) } func (c *Client) ShowListenerWithContext(ctx context.Context, req *ShowListenerRequest) (*ShowListenerResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/v4/elb/show-listener") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &ShowListenerResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/elb/api_update_listener.go ================================================ package elb import ( "context" "net/http" ) type UpdateListenerRequest struct { ClientToken *string `json:"clientToken,omitempty"` RegionID *string `json:"regionID,omitempty"` ListenerID *string `json:"listenerID,omitempty"` Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` CertificateID *string `json:"certificateID,omitempty"` CaEnabled *bool `json:"caEnabled,omitempty"` ClientCertificateID *string `json:"clientCertificateID,omitempty"` } type UpdateListenerResponse struct { sdkResponseBase ReturnObj []*ListenerRecord `json:"returnObj,omitempty"` } func (c *Client) UpdateListener(req *UpdateListenerRequest) (*UpdateListenerResponse, error) { return c.UpdateListenerWithContext(context.Background(), req) } func (c *Client) UpdateListenerWithContext(ctx context.Context, req *UpdateListenerRequest) (*UpdateListenerResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/v4/elb/update-listener") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &UpdateListenerResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/elb/client.go ================================================ package elb import ( "fmt" "time" "github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/openapi" "github.com/go-resty/resty/v2" ) const endpoint = "https://ctelb-global.ctapi.ctyun.cn" type Client struct { client *openapi.Client } func NewClient(accessKeyId, secretAccessKey string) (*Client, error) { client, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey) if err != nil { return nil, err } return &Client{client: client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { return c.client.NewRequest(method, path) } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { return c.client.DoRequest(req) } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { resp, err := c.client.DoRequestWithResult(req, res) if err == nil { statusCode := res.GetStatusCode() errorCode := res.GetError() if (statusCode != "" && statusCode != "200" && statusCode != "800") || (errorCode != "" && errorCode != "SUCCESS") { return resp, fmt.Errorf("sdkerr: api error: code='%s', message='%s', errorCode='%s', description='%s'", statusCode, res.GetMessage(), res.GetMessage(), res.GetDescription()) } } return resp, err } ================================================ FILE: pkg/sdk3rd/ctyun/elb/types.go ================================================ package elb import ( "bytes" "encoding/json" "strconv" ) type sdkResponse interface { GetStatusCode() string GetMessage() string GetError() string GetDescription() string } type sdkResponseBase struct { StatusCode json.RawMessage `json:"statusCode,omitempty"` Message *string `json:"message,omitempty"` Error *string `json:"error,omitempty"` Description *string `json:"description,omitempty"` RequestId *string `json:"requestId,omitempty"` } func (r *sdkResponseBase) GetStatusCode() string { if r.StatusCode == nil { return "" } decoder := json.NewDecoder(bytes.NewReader(r.StatusCode)) token, err := decoder.Token() if err != nil { return "" } switch t := token.(type) { case string: return t case float64: return strconv.FormatFloat(t, 'f', -1, 64) case json.Number: return t.String() default: return "" } } func (r *sdkResponseBase) GetMessage() string { if r.Message == nil { return "" } return *r.Message } func (r *sdkResponseBase) GetError() string { if r.Error == nil { return "" } return *r.Error } func (r *sdkResponseBase) GetDescription() string { if r.Description == nil { return "" } return *r.Description } var _ sdkResponse = (*sdkResponseBase)(nil) type CertificateRecord struct { ID string `json:"ID"` RegionID string `json:"regionID"` AzName string `json:"azName"` ProjectID string `json:"projectID"` Name string `json:"name"` Description string `json:"description"` Type string `json:"type"` Certificate string `json:"certificate"` PrivateKey string `json:"privateKey"` Status string `json:"status"` CreatedTime string `json:"createdTime"` UpdatedTime string `json:"updatedTime"` } type ListenerRecord struct { ID string `json:"ID"` RegionID string `json:"regionID"` AzName string `json:"azName"` ProjectID string `json:"projectID"` Name string `json:"name"` Description string `json:"description"` LoadBalancerID string `json:"loadBalancerID"` Protocol string `json:"protocol"` ProtocolPort int32 `json:"protocolPort"` CertificateID string `json:"certificateID,omitempty"` CaEnabled bool `json:"caEnabled"` ClientCertificateID string `json:"clientCertificateID,omitempty"` Status string `json:"status"` CreatedTime string `json:"createdTime"` UpdatedTime string `json:"updatedTime"` } ================================================ FILE: pkg/sdk3rd/ctyun/faas/api_get_custom_domain.go ================================================ package faas import ( "context" "fmt" "net/http" "net/url" qs "github.com/google/go-querystring/query" ) type GetCustomDomainRequest struct { RegionId *string `json:"-" url:"-"` DomainName *string `json:"domainName,omitempty" url:"-"` CnameCheck *bool `json:"cnameCheck,omitempty" url:"cnameCheck,omitempty"` } type GetCustomDomainResponse struct { sdkResponseBase ReturnObj *CustomDomainRecord `json:"returnObj,omitempty"` } func (c *Client) GetCustomDomain(req *GetCustomDomainRequest) (*GetCustomDomainResponse, error) { return c.GetCustomDomainWithContext(context.Background(), req) } func (c *Client) GetCustomDomainWithContext(ctx context.Context, req *GetCustomDomainRequest) (*GetCustomDomainResponse, error) { httpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf("/openapi/v1/domains/customdomains/%s", url.PathEscape(*req.DomainName))) if err != nil { return nil, err } else { if req.RegionId != nil { httpreq.SetHeader("regionId", *req.RegionId) } values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &GetCustomDomainResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/faas/api_update_custom_domain.go ================================================ package faas import ( "context" "fmt" "net/http" "net/url" ) type UpdateCustomDomainRequest struct { RegionId *string `json:"-"` DomainName *string `json:"domainName,omitempty"` Protocol *string `json:"protocol,omitempty"` AuthConfig *CustomDomainAuthConfig `json:"authConfig,omitempty"` CertConfig *CustomDomainCertConfig `json:"certConfig,omitempty"` RouteConfig *CustomDomainRouteConfig `json:"routeConfig,omitempty"` } type UpdateCustomDomainResponse struct { sdkResponseBase ReturnObj *CustomDomainRecord `json:"returnObj,omitempty"` } func (c *Client) UpdateCustomDomain(req *UpdateCustomDomainRequest) (*UpdateCustomDomainResponse, error) { return c.UpdateCustomDomainWithContext(context.Background(), req) } func (c *Client) UpdateCustomDomainWithContext(ctx context.Context, req *UpdateCustomDomainRequest) (*UpdateCustomDomainResponse, error) { httpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf("/openapi/v1/domains/customdomains/%s", url.PathEscape(*req.DomainName))) if err != nil { return nil, err } else { if req.RegionId != nil { httpreq.SetHeader("regionId", *req.RegionId) } httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &UpdateCustomDomainResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/faas/client.go ================================================ package faas import ( "fmt" "time" "github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/openapi" "github.com/go-resty/resty/v2" ) const endpoint = "https://cf-global.ctapi.ctyun.cn" type Client struct { client *openapi.Client } func NewClient(accessKeyId, secretAccessKey string) (*Client, error) { client, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey) if err != nil { return nil, err } return &Client{client: client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { return c.client.NewRequest(method, path) } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { return c.client.DoRequest(req) } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { resp, err := c.client.DoRequestWithResult(req, res) if err == nil { if tcode := res.GetStatusCode(); tcode != "" && tcode != "0" { return resp, fmt.Errorf("sdkerr: api error: code='%s', message='%s', errorCode='%s', errorMessage='%s'", tcode, res.GetMessage(), res.GetMessage(), res.GetErrorMessage()) } } return resp, err } ================================================ FILE: pkg/sdk3rd/ctyun/faas/types.go ================================================ package faas import ( "bytes" "encoding/json" "strconv" ) type sdkResponse interface { GetStatusCode() string GetMessage() string GetError() string GetErrorMessage() string } type sdkResponseBase struct { StatusCode json.RawMessage `json:"statusCode,omitempty"` Message *string `json:"message,omitempty"` Error *string `json:"error,omitempty"` ErrorMessage *string `json:"errorMessage,omitempty"` RequestId *string `json:"requestId,omitempty"` } func (r *sdkResponseBase) GetStatusCode() string { if r.StatusCode == nil { return "" } decoder := json.NewDecoder(bytes.NewReader(r.StatusCode)) token, err := decoder.Token() if err != nil { return "" } switch t := token.(type) { case string: return t case float64: return strconv.FormatFloat(t, 'f', -1, 64) case json.Number: return t.String() default: return "" } } func (r *sdkResponseBase) GetMessage() string { if r.Message == nil { return "" } return *r.Message } func (r *sdkResponseBase) GetError() string { if r.Error == nil { return "" } return *r.Error } func (r *sdkResponseBase) GetErrorMessage() string { if r.ErrorMessage == nil { return "" } return *r.ErrorMessage } var _ sdkResponse = (*sdkResponseBase)(nil) type CustomDomainRecord struct { DomainName string `json:"domainName"` Protocol string `json:"protocol"` AuthConfig *CustomDomainAuthConfig `json:"authConfig,omitempty"` CertConfig *CustomDomainCertConfig `json:"certConfig,omitempty"` RouteConfig *CustomDomainRouteConfig `json:"routeConfig,omitempty"` DomainStatus string `json:"domainStatus"` CnameValid bool `json:"cnameValid"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` } type CustomDomainAuthConfig struct { AuthType string `json:"authType"` JwtConfig *CustomDomainAuthJwtConfig `json:"jwtConfig,omitempty"` } type CustomDomainAuthJwtConfig struct { Jwks string `json:"jwks"` TokenConfig *CustomDomainAuthJwtTokenConfig `json:"tokenConfig,omitempty"` ClaimTrans []*CustomDomainAuthJwtClaimTran `json:"claimTrans,omitempty"` MatchMode *CustomDomainAuthJwtMatchModeConfig `json:"matchMode,omitempty"` } type CustomDomainAuthJwtClaimTran struct { ClaimName string `json:"claimName"` TargetName string `json:"targetName"` TransLocation string `json:"transLocation"` } type CustomDomainAuthJwtTokenConfig struct { Location string `json:"location"` Name string `json:"name"` RemovePrefix *string `json:"removePrefix,omitempty"` } type CustomDomainAuthJwtMatchModeConfig struct { Mode string `json:"mode"` Path []string `json:"path"` } type CustomDomainCertConfig struct { CertName string `json:"certName"` Certificate string `json:"certificate"` PrivateKey string `json:"privateKey"` } type CustomDomainRouteConfig struct { Routes []*CustomDomainRoutePathConfig `json:"routes"` } type CustomDomainRoutePathConfig struct { EnableJwt int32 `json:"enableJwt"` FunctionId int64 `json:"functionId"` FunctionName string `json:"functionName"` FunctionUniqueName string `json:"functionUniqueName"` Methods []string `json:"methods"` Path string `json:"path"` Qualifier string `json:"qualifier,omitempty"` } ================================================ FILE: pkg/sdk3rd/ctyun/icdn/api_create_cert.go ================================================ package icdn import ( "context" "net/http" ) type CreateCertRequest struct { Name *string `json:"name,omitempty"` Certs *string `json:"certs,omitempty"` Key *string `json:"key,omitempty"` } type CreateCertResponse struct { sdkResponseBase ReturnObj *struct { Id int64 `json:"id"` } `json:"returnObj,omitempty"` } func (c *Client) CreateCert(req *CreateCertRequest) (*CreateCertResponse, error) { return c.CreateCertWithContext(context.Background(), req) } func (c *Client) CreateCertWithContext(ctx context.Context, req *CreateCertRequest) (*CreateCertResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/v1/cert/creat-cert") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &CreateCertResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/icdn/api_query_cert_detail.go ================================================ package icdn import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type QueryCertDetailRequest struct { Id *int64 `json:"id,omitempty" url:"id,omitempty"` Name *string `json:"name,omitempty" url:"name,omitempty"` UsageMode *int32 `json:"usage_mode,omitempty" url:"usage_mode,omitempty"` } type QueryCertDetailResponse struct { sdkResponseBase ReturnObj *struct { Result *CertDetail `json:"result,omitempty"` } `json:"returnObj,omitempty"` } func (c *Client) QueryCertDetail(req *QueryCertDetailRequest) (*QueryCertDetailResponse, error) { return c.QueryCertDetailWithContext(context.Background(), req) } func (c *Client) QueryCertDetailWithContext(ctx context.Context, req *QueryCertDetailRequest) (*QueryCertDetailResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/v1/cert/query-cert-detail") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &QueryCertDetailResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/icdn/api_query_cert_list.go ================================================ package icdn import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type QueryCertListRequest struct { Page *int32 `json:"page,omitempty" url:"page,omitempty"` PerPage *int32 `json:"per_page,omitempty" url:"per_page,omitempty"` UsageMode *int32 `json:"usage_mode,omitempty" url:"usage_mode,omitempty"` } type QueryCertListResponse struct { sdkResponseBase ReturnObj *struct { Results []*CertRecord `json:"result,omitempty"` Page int32 `json:"page,omitempty"` PerPage int32 `json:"per_page,omitempty"` TotalPage int32 `json:"total_page,omitempty"` TotalRecords int32 `json:"total_records,omitempty"` } `json:"returnObj,omitempty"` } func (c *Client) QueryCertList(req *QueryCertListRequest) (*QueryCertListResponse, error) { return c.QueryCertListWithContext(context.Background(), req) } func (c *Client) QueryCertListWithContext(ctx context.Context, req *QueryCertListRequest) (*QueryCertListResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/v1/cert/query-cert-list") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &QueryCertListResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/icdn/api_query_domain_detail.go ================================================ package icdn import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type QueryDomainDetailRequest struct { Domain *string `json:"domain,omitempty" url:"domain,omitempty"` ProductCode *string `json:"product_code,omitempty" url:"product_code,omitempty"` FunctionNames *string `json:"function_names,omitempty" url:"function_names,omitempty"` } type QueryDomainDetailResponse struct { sdkResponseBase ReturnObj *DomainDetail `json:"returnObj,omitempty"` } func (c *Client) QueryDomainDetail(req *QueryDomainDetailRequest) (*QueryDomainDetailResponse, error) { return c.QueryDomainDetailWithContext(context.Background(), req) } func (c *Client) QueryDomainDetailWithContext(ctx context.Context, req *QueryDomainDetailRequest) (*QueryDomainDetailResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/v1/domain/query-domain-detail") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &QueryDomainDetailResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/icdn/api_query_domain_list.go ================================================ package icdn import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type QueryDomainListRequest struct { Page *int32 `json:"page,omitempty" url:"page,omitempty"` PageSize *int32 `json:"page_size,omitempty" url:"page_size,omitempty"` Domain *string `json:"domain,omitempty" url:"domain,omitempty"` ProductCode *string `json:"product_code,omitempty" url:"product_code,omitempty"` Status *int32 `json:"status,omitempty" url:"status,omitempty"` AreaScope *int32 `json:"area_scope,omitempty" url:"area_scope,omitempty"` } type QueryDomainListResponse struct { sdkResponseBase ReturnObj *struct { Results []*DomainRecord `json:"result,omitempty"` Page int32 `json:"page,omitempty"` PageSize int32 `json:"page_size,omitempty"` PageCount int32 `json:"page_count,omitempty"` Total int32 `json:"total,omitempty"` } `json:"returnObj,omitempty"` } func (c *Client) QueryDomainList(req *QueryDomainListRequest) (*QueryDomainListResponse, error) { return c.QueryDomainListWithContext(context.Background(), req) } func (c *Client) QueryDomainListWithContext(ctx context.Context, req *QueryDomainListRequest) (*QueryDomainListResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/v1/domain/query-domain-list") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &QueryDomainListResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/icdn/api_update_domain.go ================================================ package icdn import ( "context" "net/http" ) type UpdateDomainRequest struct { Domain *string `json:"domain,omitempty"` HttpsStatus *string `json:"https_status,omitempty"` CertName *string `json:"cert_name,omitempty"` } type UpdateDomainResponse struct { sdkResponseBase } func (c *Client) UpdateDomain(req *UpdateDomainRequest) (*UpdateDomainResponse, error) { return c.UpdateDomainWithContext(context.Background(), req) } func (c *Client) UpdateDomainWithContext(ctx context.Context, req *UpdateDomainRequest) (*UpdateDomainResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/v1/domain/update-domain") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &UpdateDomainResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/icdn/client.go ================================================ package icdn import ( "fmt" "time" "github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/openapi" "github.com/go-resty/resty/v2" ) const endpoint = "https://icdn-global.ctapi.ctyun.cn" type Client struct { client *openapi.Client } func NewClient(accessKeyId, secretAccessKey string) (*Client, error) { client, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey) if err != nil { return nil, err } return &Client{client: client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { return c.client.NewRequest(method, path) } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { return c.client.DoRequest(req) } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { resp, err := c.client.DoRequestWithResult(req, res) if err == nil { if tcode := res.GetStatusCode(); tcode != "" && tcode != "100000" { return resp, fmt.Errorf("sdkerr: api error: code='%s', message='%s', errorCode='%s', errorMessage='%s'", tcode, res.GetMessage(), res.GetMessage(), res.GetErrorMessage()) } } return resp, err } ================================================ FILE: pkg/sdk3rd/ctyun/icdn/types.go ================================================ package icdn import ( "bytes" "encoding/json" "strconv" ) type sdkResponse interface { GetStatusCode() string GetMessage() string GetError() string GetErrorMessage() string } type sdkResponseBase struct { StatusCode json.RawMessage `json:"statusCode,omitempty"` Message *string `json:"message,omitempty"` Error *string `json:"error,omitempty"` ErrorMessage *string `json:"errorMessage,omitempty"` RequestId *string `json:"requestId,omitempty"` } func (r *sdkResponseBase) GetStatusCode() string { if r.StatusCode == nil { return "" } decoder := json.NewDecoder(bytes.NewReader(r.StatusCode)) token, err := decoder.Token() if err != nil { return "" } switch t := token.(type) { case string: return t case float64: return strconv.FormatFloat(t, 'f', -1, 64) case json.Number: return t.String() default: return "" } } func (r *sdkResponseBase) GetMessage() string { if r.Message == nil { return "" } return *r.Message } func (r *sdkResponseBase) GetError() string { if r.Error == nil { return "" } return *r.Error } func (r *sdkResponseBase) GetErrorMessage() string { if r.ErrorMessage == nil { return "" } return *r.ErrorMessage } var _ sdkResponse = (*sdkResponseBase)(nil) type DomainRecord struct { Domain string `json:"domain"` Cname string `json:"cname"` ProductCode string `json:"product_code"` ProductName string `json:"product_name"` AreaScope int32 `json:"area_scope"` Status int32 `json:"status"` } type DomainDetail struct { DomainRecord HttpsStatus string `json:"https_status"` HttpsBasic *DomainHttpsBasicConfig `json:"https_basic,omitempty"` CertName string `json:"cert_name"` Ssl string `json:"ssl"` SslStapling string `json:"ssl_stapling"` } type DomainHttpsBasicConfig struct { HttpsForce string `json:"https_force"` HttpForce string `json:"http_force"` ForceStatus string `json:"force_status"` OriginProtocol string `json:"origin_protocol"` } type CertRecord struct { Id int64 `json:"id"` Name string `json:"name"` CN string `json:"cn"` SANs []string `json:"sans"` UsageMode int32 `json:"usage_mode"` State int32 `json:"state"` ExpiresTime int64 `json:"expires"` IssueTime int64 `json:"issue"` Issuer string `json:"issuer"` CreatedTime int64 `json:"created"` } type CertDetail struct { CertRecord Certs string `json:"certs"` Key string `json:"key"` } ================================================ FILE: pkg/sdk3rd/ctyun/lvdn/api_create_cert.go ================================================ package lvdn import ( "context" "net/http" ) type CreateCertRequest struct { Name *string `json:"name,omitempty"` Certs *string `json:"certs,omitempty"` Key *string `json:"key,omitempty"` } type CreateCertResponse struct { sdkResponseBase ReturnObj *struct { Id int64 `json:"id"` } `json:"returnObj,omitempty"` } func (c *Client) CreateCert(req *CreateCertRequest) (*CreateCertResponse, error) { return c.CreateCertWithContext(context.Background(), req) } func (c *Client) CreateCertWithContext(ctx context.Context, req *CreateCertRequest) (*CreateCertResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/cert/creat-cert") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &CreateCertResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/lvdn/api_query_cert_detail.go ================================================ package lvdn import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type QueryCertDetailRequest struct { Id *int64 `json:"id,omitempty" url:"id,omitempty"` Name *string `json:"name,omitempty" url:"name,omitempty"` UsageMode *int32 `json:"usage_mode,omitempty" url:"usage_mode,omitempty"` } type QueryCertDetailResponse struct { sdkResponseBase ReturnObj *struct { Result *CertDetail `json:"result,omitempty"` } `json:"returnObj,omitempty"` } func (c *Client) QueryCertDetail(req *QueryCertDetailRequest) (*QueryCertDetailResponse, error) { return c.QueryCertDetailWithContext(context.Background(), req) } func (c *Client) QueryCertDetailWithContext(ctx context.Context, req *QueryCertDetailRequest) (*QueryCertDetailResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/cert/query-cert-detail") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &QueryCertDetailResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/lvdn/api_query_cert_list.go ================================================ package lvdn import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type QueryCertListRequest struct { Page *int32 `json:"page,omitempty" url:"page,omitempty"` PerPage *int32 `json:"per_page,omitempty" url:"per_page,omitempty"` UsageMode *int32 `json:"usage_mode,omitempty" url:"usage_mode,omitempty"` } type QueryCertListResponse struct { sdkResponseBase ReturnObj *struct { Results []*CertRecord `json:"result,omitempty"` Page int32 `json:"page,omitempty"` PerPage int32 `json:"per_page,omitempty"` TotalPage int32 `json:"total_page,omitempty"` TotalRecords int32 `json:"total_records,omitempty"` } `json:"returnObj,omitempty"` } func (c *Client) QueryCertList(req *QueryCertListRequest) (*QueryCertListResponse, error) { return c.QueryCertListWithContext(context.Background(), req) } func (c *Client) QueryCertListWithContext(ctx context.Context, req *QueryCertListRequest) (*QueryCertListResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/cert/query-cert-list") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &QueryCertListResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/lvdn/api_query_domain_detail.go ================================================ package lvdn import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type QueryDomainDetailRequest struct { Domain *string `json:"domain,omitempty" url:"domain,omitempty"` ProductCode *string `json:"product_code,omitempty" url:"product_code,omitempty"` } type QueryDomainDetailResponse struct { sdkResponseBase ReturnObj *DomainDetail `json:"returnObj,omitempty"` } func (c *Client) QueryDomainDetail(req *QueryDomainDetailRequest) (*QueryDomainDetailResponse, error) { return c.QueryDomainDetailWithContext(context.Background(), req) } func (c *Client) QueryDomainDetailWithContext(ctx context.Context, req *QueryDomainDetailRequest) (*QueryDomainDetailResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/live/domain/query-domain-detail") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &QueryDomainDetailResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/lvdn/api_query_domain_list.go ================================================ package lvdn import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type QueryDomainListRequest struct { Page *int32 `json:"page,omitempty" url:"page,omitempty"` PageSize *int32 `json:"page_size,omitempty" url:"page_size,omitempty"` Domain *string `json:"domain,omitempty" url:"domain,omitempty"` ProductCode *string `json:"product_code,omitempty" url:"product_code,omitempty"` Status *int32 `json:"status,omitempty" url:"status,omitempty"` AreaScope *int32 `json:"area_scope,omitempty" url:"area_scope,omitempty"` } type QueryDomainListResponse struct { sdkResponseBase ReturnObj *struct { Results []*DomainRecord `json:"result,omitempty"` Page int32 `json:"page,omitempty"` PageSize int32 `json:"page_size,omitempty"` PageCount int32 `json:"page_count,omitempty"` Total int32 `json:"total,omitempty"` } `json:"returnObj,omitempty"` } func (c *Client) QueryDomainList(req *QueryDomainListRequest) (*QueryDomainListResponse, error) { return c.QueryDomainListWithContext(context.Background(), req) } func (c *Client) QueryDomainListWithContext(ctx context.Context, req *QueryDomainListRequest) (*QueryDomainListResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/v1/domain/query-domain-list") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &QueryDomainListResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/lvdn/api_update_domain.go ================================================ package lvdn import ( "context" "net/http" ) type UpdateDomainRequest struct { Domain *string `json:"domain,omitempty"` ProductCode *string `json:"product_code,omitempty"` HttpsSwitch *int32 `json:"https_switch,omitempty"` CertName *string `json:"cert_name,omitempty"` } type UpdateDomainResponse struct { sdkResponseBase } func (c *Client) UpdateDomain(req *UpdateDomainRequest) (*UpdateDomainResponse, error) { return c.UpdateDomainWithContext(context.Background(), req) } func (c *Client) UpdateDomainWithContext(ctx context.Context, req *UpdateDomainRequest) (*UpdateDomainResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/live/domain/update-domain") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &UpdateDomainResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ctyun/lvdn/client.go ================================================ package lvdn import ( "fmt" "time" "github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/openapi" "github.com/go-resty/resty/v2" ) const endpoint = "https://ctlvdn-global.ctapi.ctyun.cn" type Client struct { client *openapi.Client } func NewClient(accessKeyId, secretAccessKey string) (*Client, error) { client, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey) if err != nil { return nil, err } return &Client{client: client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { return c.client.NewRequest(method, path) } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { return c.client.DoRequest(req) } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { resp, err := c.client.DoRequestWithResult(req, res) if err == nil { if tcode := res.GetStatusCode(); tcode != "" && tcode != "100000" { return resp, fmt.Errorf("sdkerr: api error: code='%s', message='%s', errorCode='%s', errorMessage='%s'", tcode, res.GetMessage(), res.GetMessage(), res.GetErrorMessage()) } } return resp, err } ================================================ FILE: pkg/sdk3rd/ctyun/lvdn/types.go ================================================ package lvdn import ( "bytes" "encoding/json" "strconv" ) type sdkResponse interface { GetStatusCode() string GetMessage() string GetError() string GetErrorMessage() string } type sdkResponseBase struct { StatusCode json.RawMessage `json:"statusCode,omitempty"` Message *string `json:"message,omitempty"` Error *string `json:"error,omitempty"` ErrorMessage *string `json:"errorMessage,omitempty"` RequestId *string `json:"requestId,omitempty"` } func (r *sdkResponseBase) GetStatusCode() string { if r.StatusCode == nil { return "" } decoder := json.NewDecoder(bytes.NewReader(r.StatusCode)) token, err := decoder.Token() if err != nil { return "" } switch t := token.(type) { case string: return t case float64: return strconv.FormatFloat(t, 'f', -1, 64) case json.Number: return t.String() default: return "" } } func (r *sdkResponseBase) GetMessage() string { if r.Message == nil { return "" } return *r.Message } func (r *sdkResponseBase) GetError() string { if r.Error == nil { return "" } return *r.Error } func (r *sdkResponseBase) GetErrorMessage() string { if r.ErrorMessage == nil { return "" } return *r.ErrorMessage } var _ sdkResponse = (*sdkResponseBase)(nil) type DomainRecord struct { Domain string `json:"domain"` Cname string `json:"cname"` ProductCode string `json:"product_code"` ProductName string `json:"product_name"` AreaScope int32 `json:"area_scope"` Status int32 `json:"status"` } type DomainDetail struct { DomainRecord HttpsSwitch int32 `json:"https_switch"` CertName string `json:"cert_name"` } type CertRecord struct { Id int64 `json:"id"` Name string `json:"name"` CN string `json:"cn"` SANs []string `json:"sans"` UsageMode int32 `json:"usage_mode"` State int32 `json:"state"` ExpiresTime int64 `json:"expires"` IssueTime int64 `json:"issue"` Issuer string `json:"issuer"` CreatedTime int64 `json:"created"` } type CertDetail struct { CertRecord Certs string `json:"certs"` Key string `json:"key"` } ================================================ FILE: pkg/sdk3rd/ctyun/openapi/client.go ================================================ package openapi import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "net/url" "time" "github.com/go-resty/resty/v2" "github.com/pocketbase/pocketbase/tools/security" "github.com/certimate-go/certimate/internal/app" ) type Client struct { client *resty.Client } func NewClient(endpoint, accessKeyId, secretAccessKey string) (*Client, error) { if endpoint == "" { return nil, fmt.Errorf("sdkerr: unset endpoint") } if _, err := url.Parse(endpoint); err != nil { return nil, fmt.Errorf("sdkerr: invalid endpoint: %w", err) } if accessKeyId == "" { return nil, fmt.Errorf("sdkerr: unset accessKeyId") } if secretAccessKey == "" { return nil, fmt.Errorf("sdkerr: unset secretAccessKey") } client := resty.New(). SetBaseURL(endpoint). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { // 生成时间戳及流水号 now := time.Now() eopDate := now.Format("20060102T150405Z") eopReqId := security.RandomString(32) // 获取查询参数 queryStr := "" if req.URL != nil { queryStr = req.URL.Query().Encode() } // 获取请求正文 payloadStr := "" if req.Body != nil { reader, err := req.GetBody() if err != nil { return err } defer reader.Close() payload, err := io.ReadAll(reader) if err != nil { return err } payloadStr = string(payload) } // 构造代签字符串 payloadHash := sha256.Sum256([]byte(payloadStr)) payloadHashHex := hex.EncodeToString(payloadHash[:]) dataToSign := fmt.Sprintf("ctyun-eop-request-id:%s\neop-date:%s\n\n%s\n%s", eopReqId, eopDate, queryStr, payloadHashHex) // 生成 ktime hasher := hmac.New(sha256.New, []byte(secretAccessKey)) hasher.Write([]byte(eopDate)) ktime := hasher.Sum(nil) // 生成 kak hasher = hmac.New(sha256.New, ktime) hasher.Write([]byte(accessKeyId)) kak := hasher.Sum(nil) // 生成 kdate hasher = hmac.New(sha256.New, kak) hasher.Write([]byte(now.Format("20060102"))) kdate := hasher.Sum(nil) // 构造签名 hasher = hmac.New(sha256.New, kdate) hasher.Write([]byte(dataToSign)) sign := hasher.Sum(nil) signStr := base64.StdEncoding.EncodeToString(sign) // 设置请求头 req.Header.Set("ctyun-eop-request-id", eopReqId) req.Header.Set("eop-date", eopDate) req.Header.Set("eop-authorization", fmt.Sprintf("%s Headers=ctyun-eop-request-id;eop-date Signature=%s", accessKeyId, signStr)) return nil }) return &Client{client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) NewRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) DoRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) DoRequestWithResult(req *resty.Request, res interface{}) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.DoRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } } return resp, nil } ================================================ FILE: pkg/sdk3rd/dcloud/unicloud/api_create_domain_with_cert.go ================================================ package unicloud import ( "net/http" ) type CreateDomainWithCertRequest struct { Provider string `json:"provider"` SpaceId string `json:"spaceId"` Domain string `json:"domain"` Cert string `json:"cert"` Key string `json:"key"` } type CreateDomainWithCertResponse struct { sdkResponseBase } func (c *Client) CreateDomainWithCert(req *CreateDomainWithCertRequest) (*CreateDomainWithCertResponse, error) { if err := c.ensureApiUserTokenExists(); err != nil { return nil, err } resp := &CreateDomainWithCertResponse{} err := c.sendRequestWithResult(http.MethodPost, "/host/create-domain-with-cert", req, resp) return resp, err } ================================================ FILE: pkg/sdk3rd/dcloud/unicloud/client.go ================================================ package unicloud import ( "crypto/hmac" "crypto/md5" "encoding/hex" "encoding/json" "fmt" "net/http" "regexp" "runtime" "sort" "strings" "sync" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { username string password string serverlessJwtToken string serverlessJwtTokenExp time.Time serverlessJwtTokenMtx sync.Mutex serverlessClient *resty.Client apiUserToken string apiUserTokenMtx sync.Mutex apiClient *resty.Client } const ( uniIdentityEndpoint = "https://account.dcloud.net.cn/client" uniIdentityClientSecret = "ba461799-fde8-429f-8cc4-4b6d306e2339" uniIdentityAppId = "__UNI__uniid_server" uniIdentitySpaceId = "uni-id-server" uniConsoleEndpoint = "https://unicloud.dcloud.net.cn/client" uniConsoleClientSecret = "4c1f7fbf-c732-42b0-ab10-4634a8bbe834" uniConsoleAppId = "__UNI__unicloud_console" uniConsoleSpaceId = "dc-6nfabcn6ada8d3dd" ) func NewClient(username, password string) (*Client, error) { if username == "" { return nil, fmt.Errorf("sdkerr: unset username") } if password == "" { return nil, fmt.Errorf("sdkerr: unset password") } client := &Client{ username: username, password: password, } client.serverlessClient = resty.New(). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent) client.apiClient = resty.New(). SetBaseURL("https://unicloud-api.dcloud.net.cn/unicloud/api"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { if client.apiUserToken != "" { req.Header.Set("Token", client.apiUserToken) } return nil }) return client, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.serverlessClient.SetTimeout(timeout) return c } func (c *Client) buildServerlessClientInfo(appId string) (_clientInfo map[string]any, _err error) { return map[string]any{ "PLATFORM": "web", "OS": strings.ToUpper(runtime.GOOS), "APPID": appId, "DEVICEID": app.AppName, "LOCALE": "zh-Hans", "osName": runtime.GOOS, "appId": appId, "appName": "uniCloud", "deviceId": app.AppName, "deviceType": "pc", "uniPlatform": "web", "uniCompilerVersion": "4.45", "uniRuntimeVersion": "4.45", }, nil } func (c *Client) buildServerlessPayloadInfo(appId, spaceId, target, method, action string, params, data interface{}) (map[string]any, error) { clientInfo, err := c.buildServerlessClientInfo(appId) if err != nil { return nil, err } functionArgsParams := make([]any, 0) if params != nil { functionArgsParams = append(functionArgsParams, params) } functionArgs := map[string]any{ "clientInfo": clientInfo, "uniIdToken": c.serverlessJwtToken, } if method != "" { functionArgs["method"] = method functionArgs["params"] = make([]any, 0) } if action != "" { type _obj struct{} functionArgs["action"] = action functionArgs["data"] = &_obj{} } if params != nil { functionArgs["params"] = []any{params} } if data != nil { functionArgs["data"] = data } jsonb, err := json.Marshal(map[string]any{ "functionTarget": target, "functionArgs": functionArgs, }) if err != nil { return nil, err } payload := map[string]any{ "method": "serverless.function.runtime.invoke", "params": string(jsonb), "spaceId": spaceId, "timestamp": time.Now().UnixMilli(), } return payload, nil } func (c *Client) invokeServerless(endpoint, clientSecret, appId, spaceId, target, method, action string, params, data interface{}) (*resty.Response, error) { if endpoint == "" { return nil, fmt.Errorf("unicloud api error: endpoint cannot be empty") } payload, err := c.buildServerlessPayloadInfo(appId, spaceId, target, method, action, params, data) if err != nil { return nil, fmt.Errorf("unicloud api error: failed to build request: %w", err) } clientInfo, _ := c.buildServerlessClientInfo(appId) clientInfoJsonb, _ := json.Marshal(clientInfo) sign := generateSignature(payload, clientSecret) req := c.serverlessClient.R(). SetHeader("Content-Type", "application/json"). SetHeader("Origin", "https://unicloud.dcloud.net.cn"). SetHeader("Referer", "https://unicloud.dcloud.net.cn"). SetHeader("X-Client-Info", string(clientInfoJsonb)). SetHeader("X-Client-Token", c.serverlessJwtToken). SetHeader("X-Serverless-Sign", sign). SetBody(payload) resp, err := req.Post(endpoint) if err != nil { return resp, fmt.Errorf("unicloud api error: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("unicloud api error: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) invokeServerlessWithResult(endpoint, clientSecret, appId, spaceId, target, method, action string, params, data interface{}, result sdkResponse) error { resp, err := c.invokeServerless(endpoint, clientSecret, appId, spaceId, target, method, action, params, data) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &result) } return err } if err := json.Unmarshal(resp.Body(), &result); err != nil { return fmt.Errorf("unicloud api error: failed to unmarshal response: %w", err) } else if success := result.GetSuccess(); !success { return fmt.Errorf("unicloud api error: code='%s', message='%s'", result.GetErrorCode(), result.GetErrorMessage()) } return nil } func (c *Client) sendRequest(method string, path string, params interface{}) (*resty.Response, error) { req := c.apiClient.R() if strings.EqualFold(method, http.MethodGet) { qs := make(map[string]string) if params != nil { temp := make(map[string]any) jsonb, _ := json.Marshal(params) json.Unmarshal(jsonb, &temp) for k, v := range temp { if v != nil { qs[k] = fmt.Sprintf("%v", v) } } } req = req.SetQueryParams(qs) } else { req = req.SetHeader("Content-Type", "application/json").SetBody(params) } resp, err := req.Execute(method, path) if err != nil { return resp, fmt.Errorf("unicloud api error: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("unicloud api error: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) sendRequestWithResult(method string, path string, params interface{}, result sdkResponse) error { resp, err := c.sendRequest(method, path, params) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &result) } return err } if err := json.Unmarshal(resp.Body(), &result); err != nil { return fmt.Errorf("unicloud api error: failed to unmarshal response: %w", err) } else if retcode := result.GetReturnCode(); retcode != 0 { return fmt.Errorf("unicloud api error: ret='%d', desc='%s'", retcode, result.GetReturnDesc()) } return nil } func (c *Client) ensureServerlessJwtTokenExists() error { c.serverlessJwtTokenMtx.Lock() defer c.serverlessJwtTokenMtx.Unlock() if c.serverlessJwtToken != "" && c.serverlessJwtTokenExp.After(time.Now()) { return nil } params := map[string]string{ "password": "password", } if regexp.MustCompile(`^1\d{10}$`).MatchString(c.username) { params["mobile"] = c.username } else if regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`).MatchString(c.username) { params["email"] = c.username } else { params["username"] = c.username } type loginResponse struct { sdkResponseBase Data *struct { Code int32 `json:"errCode"` UID string `json:"uid"` NewToken *struct { Token string `json:"token"` TokenExpired int64 `json:"tokenExpired"` } `json:"newToken,omitempty"` } `json:"data,omitempty"` } resp := &loginResponse{} if err := c.invokeServerlessWithResult( uniIdentityEndpoint, uniIdentityClientSecret, uniIdentityAppId, uniIdentitySpaceId, "uni-id-co", "login", "", params, nil, resp); err != nil { return err } else if resp.Data == nil || resp.Data.NewToken == nil || resp.Data.NewToken.Token == "" { return fmt.Errorf("unicloud api error: received empty token") } c.serverlessJwtToken = resp.Data.NewToken.Token c.serverlessJwtTokenExp = time.UnixMilli(resp.Data.NewToken.TokenExpired) return nil } func (c *Client) ensureApiUserTokenExists() error { if err := c.ensureServerlessJwtTokenExists(); err != nil { return err } c.apiUserTokenMtx.Lock() defer c.apiUserTokenMtx.Unlock() if c.apiUserToken != "" { return nil } type getUserTokenResponse struct { sdkResponseBase Data *struct { Code int32 `json:"code"` Data *struct { Result int32 `json:"ret"` Description string `json:"desc"` Data *struct { Email string `json:"email"` Token string `json:"token"` } `json:"data,omitempty"` } `json:"data,omitempty"` } `json:"data,omitempty"` } resp := &getUserTokenResponse{} if err := c.invokeServerlessWithResult( uniConsoleEndpoint, uniConsoleClientSecret, uniConsoleAppId, uniConsoleSpaceId, "uni-cloud-kernel", "", "user/getUserToken", nil, map[string]any{"isLogin": true}, resp); err != nil { return err } else if resp.Data == nil || resp.Data.Data == nil || resp.Data.Data.Data == nil || resp.Data.Data.Data.Token == "" { return fmt.Errorf("unicloud api error: received empty user token") } c.apiUserToken = resp.Data.Data.Data.Token return nil } func generateSignature(params map[string]any, secret string) string { keys := make([]string, 0, len(params)) for k := range params { keys = append(keys, k) } sort.Strings(keys) canonicalStr := "" for i, k := range keys { if i > 0 { canonicalStr += "&" } canonicalStr += k + "=" + fmt.Sprintf("%v", params[k]) } mac := hmac.New(md5.New, []byte(secret)) mac.Write([]byte(canonicalStr)) sign := mac.Sum(nil) signHex := hex.EncodeToString(sign) return signHex } ================================================ FILE: pkg/sdk3rd/dcloud/unicloud/types.go ================================================ package unicloud type sdkResponse interface { GetSuccess() bool GetErrorCode() string GetErrorMessage() string GetReturnCode() int GetReturnDesc() string } type sdkResponseBase struct { Success *bool `json:"success,omitempty"` Header *map[string]string `json:"header,omitempty"` Error *struct { Code string `json:"code"` Message string `json:"message"` } `json:"error,omitempty"` ReturnCode *int `json:"ret,omitempty"` ReturnDesc *string `json:"desc,omitempty"` } func (r *sdkResponseBase) GetReturnCode() int { if r.ReturnCode == nil { return 0 } return *r.ReturnCode } func (r *sdkResponseBase) GetReturnDesc() string { if r.ReturnDesc == nil { return "" } return *r.ReturnDesc } func (r *sdkResponseBase) GetSuccess() bool { if r.Success == nil { return false } return *r.Success } func (r *sdkResponseBase) GetErrorCode() string { if r.Error == nil { return "" } return r.Error.Code } func (r *sdkResponseBase) GetErrorMessage() string { if r.Error == nil { return "" } return r.Error.Message } var _ sdkResponse = (*sdkResponseBase)(nil) ================================================ FILE: pkg/sdk3rd/dnsla/api_create_record.go ================================================ package dnsla import ( "context" "net/http" ) type CreateRecordRequest struct { DomainId *string `json:"domainId"` GroupId *string `json:"groupId,omitempty"` LineId *string `json:"lineId,omitempty"` Type *int32 `json:"type"` Host *string `json:"host"` Data *string `json:"data"` Ttl *int32 `json:"ttl"` Weight *int32 `json:"weight,omitempty"` Preference *int32 `json:"preference,omitempty"` } type CreateRecordResponse struct { sdkResponseBase Data *struct { Id string `json:"id"` } `json:"data,omitempty"` } func (c *Client) CreateRecord(req *CreateRecordRequest) (*CreateRecordResponse, error) { return c.CreateRecordWithContext(context.Background(), req) } func (c *Client) CreateRecordWithContext(ctx context.Context, req *CreateRecordRequest) (*CreateRecordResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/record") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &CreateRecordResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/dnsla/api_delete_record.go ================================================ package dnsla import ( "context" "fmt" "net/http" ) type DeleteRecordResponse struct { sdkResponseBase } func (c *Client) DeleteRecord(recordId string) (*DeleteRecordResponse, error) { return c.DeleteRecordWithContext(context.Background(), recordId) } func (c *Client) DeleteRecordWithContext(ctx context.Context, recordId string) (*DeleteRecordResponse, error) { if recordId == "" { return nil, fmt.Errorf("sdkerr: unset recordId") } httpreq, err := c.newRequest(http.MethodDelete, "/record") if err != nil { return nil, err } else { httpreq.SetQueryParam("id", recordId) httpreq.SetContext(ctx) } result := &DeleteRecordResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/dnsla/api_list_domains.go ================================================ package dnsla import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type ListDomainsRequest struct { GroupId *string `json:"groupId,omitempty" url:"groupId,omitempty"` PageIndex *int32 `json:"pageIndex,omitempty" url:"pageIndex,omitempty"` PageSize *int32 `json:"pageSize,omitempty" url:"pageSize,omitempty"` } type ListDomainsResponse struct { sdkResponseBase Data *struct { Total int32 `json:"total"` Results []*DomainRecord `json:"results"` } `json:"data,omitempty"` } func (c *Client) ListDomains(req *ListDomainsRequest) (*ListDomainsResponse, error) { return c.ListDomainsWithContext(context.Background(), req) } func (c *Client) ListDomainsWithContext(ctx context.Context, req *ListDomainsRequest) (*ListDomainsResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/domainList") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &ListDomainsResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/dnsla/api_list_records.go ================================================ package dnsla import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type ListRecordsRequest struct { DomainId *string `json:"domainId,omitempty" url:"domainId,omitempty"` GroupId *string `json:"groupId,omitempty" url:"groupId,omitempty"` LineId *string `json:"lineId,omitempty" url:"lineId,omitempty"` Type *int32 `json:"type,omitempty" url:"type,omitempty"` Host *string `json:"host,omitempty" url:"host,omitempty"` Data *string `json:"data,omitempty" url:"data,omitempty"` PageIndex *int32 `json:"pageIndex,omitempty" url:"pageIndex,omitempty"` PageSize *int32 `json:"pageSize,omitempty" url:"pageSize,omitempty"` } type ListRecordsResponse struct { sdkResponseBase Data *struct { Total int32 `json:"total"` Results []*DnsRecord `json:"results"` } `json:"data,omitempty"` } func (c *Client) ListRecords(req *ListRecordsRequest) (*ListRecordsResponse, error) { return c.ListRecordsWithContext(context.Background(), req) } func (c *Client) ListRecordsWithContext(ctx context.Context, req *ListRecordsRequest) (*ListRecordsResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/recordList") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &ListRecordsResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/dnsla/api_update_record.go ================================================ package dnsla import ( "context" "net/http" ) type UpdateRecordRequest struct { Id *string `json:"id"` GroupId *string `json:"groupId,omitempty"` LineId *string `json:"lineId,omitempty"` Type *int32 `json:"type,omitempty"` Host *string `json:"host,omitempty"` Data *string `json:"data,omitempty"` Ttl *int32 `json:"ttl,omitempty"` Weight *int32 `json:"weight,omitempty"` Preference *int32 `json:"preference,omitempty"` } type UpdateRecordResponse struct { sdkResponseBase } func (c *Client) UpdateRecord(req *UpdateRecordRequest) (*UpdateRecordResponse, error) { return c.UpdateRecordWithContext(context.Background(), req) } func (c *Client) UpdateRecordWithContext(ctx context.Context, req *UpdateRecordRequest) (*UpdateRecordResponse, error) { httpreq, err := c.newRequest(http.MethodPut, "/record") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &UpdateRecordResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/dnsla/client.go ================================================ package dnsla import ( "encoding/json" "fmt" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { client *resty.Client } func NewClient(apiId, apiSecret string) (*Client, error) { if apiId == "" { return nil, fmt.Errorf("sdkerr: unset apiId") } if apiSecret == "" { return nil, fmt.Errorf("sdkerr: unset apiSecret") } client := resty.New(). SetBaseURL("https://api.dns.la/api"). SetBasicAuth(apiId, apiSecret). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent) return &Client{client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if tcode := res.GetCode(); tcode/100 != 2 { return resp, fmt.Errorf("sdkerr: code='%d', message='%s'", tcode, res.GetMessage()) } } } return resp, nil } ================================================ FILE: pkg/sdk3rd/dnsla/types.go ================================================ package dnsla type sdkResponse interface { GetCode() int GetMessage() string } type sdkResponseBase struct { Code *int `json:"code,omitempty"` Message *string `json:"message,omitempty"` } func (r *sdkResponseBase) GetCode() int { if r.Code == nil { return 0 } return *r.Code } func (r *sdkResponseBase) GetMessage() string { if r.Message == nil { return "" } return *r.Message } var _ sdkResponse = (*sdkResponseBase)(nil) type DomainRecord struct { Id string `json:"id"` GroupId string `json:"groupId"` GroupName string `json:"groupName"` Domain string `json:"domain"` DisplayDomain string `json:"displayDomain"` CreatedAt int64 `json:"createdAt"` UpdatedAt int64 `json:"updatedAt"` } type DnsRecord struct { Id string `json:"id"` DomainId string `json:"domainId"` GroupId string `json:"groupId"` GroupName string `json:"groupName"` LineId string `json:"lineId"` LineCode string `json:"lineCode"` LineName string `json:"lineName"` Type int32 `json:"type"` Host string `json:"host"` DisplayHost string `json:"displayHost"` Data string `json:"data"` DisplayData string `json:"displayData"` Ttl int32 `json:"ttl"` Weight int32 `json:"weight"` Preference int32 `json:"preference"` CreatedAt int64 `json:"createdAt"` UpdatedAt int64 `json:"updatedAt"` } ================================================ FILE: pkg/sdk3rd/dogecloud/api_bind_cdn_cert.go ================================================ package dogecloud import ( "context" "net/http" ) type BindCdnCertRequest struct { CertId int64 `json:"id"` Domain string `json:"domain"` } type BindCdnCertResponse struct { sdkResponseBase } func (c *Client) BindCdnCert(req *BindCdnCertRequest) (*BindCdnCertResponse, error) { return c.BindCdnCertWithContext(context.Background(), req) } func (c *Client) BindCdnCertWithContext(ctx context.Context, req *BindCdnCertRequest) (*BindCdnCertResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/cdn/cert/bind.json") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &BindCdnCertResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/dogecloud/api_list_cdn_domain.go ================================================ package dogecloud import ( "context" "encoding/json" "net/http" ) type ListCdnDomainResponse struct { sdkResponseBase Data *struct { Domains []*struct { Id int64 `json:"id"` Name string `json:"name"` Cname string `json:"cname"` ServiceType string `json:"service_type"` Status string `json:"status"` Source json.RawMessage `json:"source"` CreateTime string `json:"ctime"` CertId int64 `json:"cert_id"` } `json:"domains"` } `json:"data,omitempty"` } func (c *Client) ListCdnDomain() (*ListCdnDomainResponse, error) { return c.ListCdnDomainWithContext(context.Background()) } func (c *Client) ListCdnDomainWithContext(ctx context.Context) (*ListCdnDomainResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/cdn/domain/list.json") if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &ListCdnDomainResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/dogecloud/api_upload_cdn_cert.go ================================================ package dogecloud import ( "context" "net/http" ) type UploadCdnCertRequest struct { Note string `json:"note"` Certificate string `json:"cert"` PrivateKey string `json:"private"` } type UploadCdnCertResponse struct { sdkResponseBase Data *struct { Id int64 `json:"id"` } `json:"data,omitempty"` } func (c *Client) UploadCdnCert(req *UploadCdnCertRequest) (*UploadCdnCertResponse, error) { return c.UploadCdnCertWithContext(context.Background(), req) } func (c *Client) UploadCdnCertWithContext(ctx context.Context, req *UploadCdnCertRequest) (*UploadCdnCertResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/cdn/cert/upload.json") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &UploadCdnCertResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/dogecloud/client.go ================================================ package dogecloud import ( "crypto/hmac" "crypto/sha1" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { client *resty.Client } func NewClient(accessKey, secretKey string) (*Client, error) { if accessKey == "" { return nil, fmt.Errorf("sdkerr: unset accessKey") } if secretKey == "" { return nil, fmt.Errorf("sdkerr: unset secretKey") } client := resty.New(). SetBaseURL("https://api.dogecloud.com"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetPreRequestHook(func(ctx *resty.Client, req *http.Request) error { requestUrl := req.URL.Path requestQuery := req.URL.Query().Encode() if requestQuery != "" { requestUrl += "?" + requestQuery } payload := "" if req.Body != nil { reader, err := req.GetBody() if err != nil { return err } defer reader.Close() payloadb, err := io.ReadAll(reader) if err != nil { return err } payload = string(payloadb) } stringToSign := fmt.Sprintf("%s\n%s", requestUrl, payload) mac := hmac.New(sha1.New, []byte(secretKey)) mac.Write([]byte(stringToSign)) sign := hex.EncodeToString(mac.Sum(nil)) req.Header.Set("Authorization", fmt.Sprintf("TOKEN %s:%s", accessKey, sign)) return nil }) return &Client{client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if tcode := res.GetCode(); tcode != 0 && tcode != 200 { return resp, fmt.Errorf("sdkerr: code='%d', msg='%s'", tcode, res.GetMessage()) } } } return resp, nil } ================================================ FILE: pkg/sdk3rd/dogecloud/types.go ================================================ package dogecloud type sdkResponse interface { GetCode() int GetMessage() string } type sdkResponseBase struct { Code *int `json:"code,omitempty"` Message *string `json:"msg,omitempty"` } func (r *sdkResponseBase) GetCode() int { if r.Code == nil { return 0 } return *r.Code } func (r *sdkResponseBase) GetMessage() string { if r.Message == nil { return "" } return *r.Message } var _ sdkResponse = (*sdkResponseBase)(nil) ================================================ FILE: pkg/sdk3rd/dokploy/api_certificates_all.go ================================================ package dokploy import ( "context" "net/http" ) type CertificatesAllRequest struct{} type CertificatesAllResponse = []*Certificate func (c *Client) CertificatesAll(req *CertificatesAllRequest) (*CertificatesAllResponse, error) { return c.CertificatesAllWithContext(context.Background(), req) } func (c *Client) CertificatesAllWithContext(ctx context.Context, req *CertificatesAllRequest) (*CertificatesAllResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/certificates.all") if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &CertificatesAllResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/dokploy/api_certificates_create.go ================================================ package dokploy import ( "context" "net/http" ) type CertificatesCreateRequest struct { CertificateId *string `json:"certificateId,omitempty"` Name *string `json:"name,omitempty"` CertificateData *string `json:"certificateData,omitempty"` PrivateKey *string `json:"privateKey,omitempty"` OrganizationId *string `json:"organizationId,omitempty"` ServerId *string `json:"serverId,omitempty"` } type CertificatesCreateResponse = Certificate func (c *Client) CertificatesCreate(req *CertificatesCreateRequest) (*CertificatesCreateResponse, error) { return c.CertificatesCreateWithContext(context.Background(), req) } func (c *Client) CertificatesCreateWithContext(ctx context.Context, req *CertificatesCreateRequest) (*CertificatesCreateResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/certificates.create") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &CertificatesCreateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/dokploy/api_user_get.go ================================================ package dokploy import ( "context" "net/http" ) type UserGetRequest struct{} type UserGetResponse struct { Id string `json:"id"` OrganizationId string `json:"organizationId"` UserId string `json:"userId"` Role string `json:"role"` CreatedAt string `json:"createdAt"` TeamId string `json:"teamId,omitempty"` IsDefault bool `json:"isDefault"` User *struct { Id string `json:"id"` FirstName string `json:"firstName"` LastName string `json:"lastName"` Email string `json:"email"` EmailVerified bool `json:"emailVerified"` Role string `json:"role"` CreatedAt string `json:"createdAt"` } `json:"user,omitempty"` } func (c *Client) UserGet(req *UserGetRequest) (*UserGetResponse, error) { return c.UserGetWithContext(context.Background(), req) } func (c *Client) UserGetWithContext(ctx context.Context, req *UserGetRequest) (*UserGetResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/user.get") if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &UserGetResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/dokploy/client.go ================================================ package dokploy import ( "crypto/tls" "encoding/json" "fmt" "net/url" "strings" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { client *resty.Client } func NewClient(serverUrl string, apiKey string) (*Client, error) { if serverUrl == "" { return nil, fmt.Errorf("sdkerr: unset serverUrl") } if _, err := url.Parse(serverUrl); err != nil { return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err) } if apiKey == "" { return nil, fmt.Errorf("sdkerr: unset apiKey") } client := resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")+"/api"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetHeader("X-Api-Key", apiKey) return &Client{client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) SetTLSConfig(config *tls.Config) *Client { c.client.SetTLSClientConfig(config) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res interface{}) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } } return resp, nil } ================================================ FILE: pkg/sdk3rd/dokploy/types.go ================================================ package dokploy type Certificate struct { CertificateId string `json:"certificateId"` Name string `json:"name"` CertificateData string `json:"certificateData"` PrivateKey string `json:"privateKey"` CertificatePath string `json:"certificatePath,omitempty"` OrganizationId string `json:"organizationId,omitempty"` ServerId string `json:"serverId,omitempty"` } ================================================ FILE: pkg/sdk3rd/dynv6/api_add_record.go ================================================ package dynv6 import ( "context" "fmt" "net/http" ) type AddRecordRequest struct { Type *string `json:"type,omitempty"` Name *string `json:"name,omitempty"` Port *int `json:"port,omitempty"` Weight *int `json:"weight,omitempty"` Priority *int `json:"priority,omitempty"` Data *string `json:"data,omitempty"` Flags *int `json:"flags,omitempty"` Tag *string `json:"tag,omitempty"` } type AddRecordResponse DNSRecord func (c *Client) AddRecord(zoneID int64, req *AddRecordRequest) (*AddRecordResponse, error) { return c.AddRecordWithContext(context.Background(), zoneID, req) } func (c *Client) AddRecordWithContext(ctx context.Context, zoneID int64, req *AddRecordRequest) (*AddRecordResponse, error) { if zoneID == 0 { return nil, fmt.Errorf("sdkerr: unset zoneID") } httpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf("/zones/%d/records", zoneID)) if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &AddRecordResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/dynv6/api_delete_record.go ================================================ package dynv6 import ( "context" "fmt" "net/http" ) type DeleteRecordResponse DNSRecord func (c *Client) DeleteRecord(zoneID int64, recordID int64) (*DeleteRecordResponse, error) { return c.DeleteRecordWithContext(context.Background(), zoneID, recordID) } func (c *Client) DeleteRecordWithContext(ctx context.Context, zoneID int64, recordID int64) (*DeleteRecordResponse, error) { if zoneID == 0 { return nil, fmt.Errorf("sdkerr: unset zoneID") } if recordID == 0 { return nil, fmt.Errorf("sdkerr: unset recordID") } httpreq, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/zones/%d/records/%d", zoneID, recordID)) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &DeleteRecordResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/dynv6/api_list_records.go ================================================ package dynv6 import ( "context" "fmt" "net/http" ) type ListRecordsResponse []*DNSRecord func (c *Client) ListRecords(zoneID int64) (*ListRecordsResponse, error) { return c.ListRecordsWithContext(context.Background(), zoneID) } func (c *Client) ListRecordsWithContext(ctx context.Context, zoneID int64) (*ListRecordsResponse, error) { if zoneID == 0 { return nil, fmt.Errorf("sdkerr: unset zoneID") } httpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf("/zones/%d/records", zoneID)) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &ListRecordsResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/dynv6/api_list_zones.go ================================================ package dynv6 import ( "context" "net/http" ) type ListZonesResponse []*ZoneRecord func (c *Client) ListZones() (*ListZonesResponse, error) { return c.ListZonesWithContext(context.Background()) } func (c *Client) ListZonesWithContext(ctx context.Context) (*ListZonesResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/zones") if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &ListZonesResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/dynv6/client.go ================================================ package dynv6 import ( "encoding/json" "fmt" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { client *resty.Client } func NewClient(httpToken string) (*Client, error) { if httpToken == "" { return nil, fmt.Errorf("sdkerr: unset httpToken") } client := resty.New(). SetBaseURL("https://dynv6.com/api/v2"). SetHeader("Accept", "application/json"). SetHeader("Authorization", "Bearer "+httpToken). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent) return &Client{client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res interface{}) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } } return resp, nil } ================================================ FILE: pkg/sdk3rd/dynv6/types.go ================================================ package dynv6 type ZoneRecord struct { ID int64 `json:"id"` Name string `json:"name"` IPv4Address string `json:"ipv4address"` IPv6Prefix string `json:"ipv6prefix"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` } type DNSRecord struct { ID int64 `json:"id"` ZoneID int64 `json:"zoneID"` Type string `json:"type"` Name string `json:"name"` Port int `json:"port"` Weight int `json:"weight"` Priority int `json:"priority"` Data string `json:"data"` ExpandedData string `json:"expandedData"` Flags int `json:"flags,omitempty"` Tag string `json:"tag,omitempty"` } ================================================ FILE: pkg/sdk3rd/flexcdn/api_update_ssl_cert.go ================================================ package flexcdn import ( "context" "net/http" ) type UpdateSSLCertRequest struct { SSLCertId int64 `json:"sslCertId"` IsOn bool `json:"isOn"` Name string `json:"name"` Description string `json:"description"` ServerName string `json:"serverName"` IsCA bool `json:"isCA"` CertData string `json:"certData"` KeyData string `json:"keyData"` TimeBeginAt int64 `json:"timeBeginAt"` TimeEndAt int64 `json:"timeEndAt"` DNSNames []string `json:"dnsNames"` CommonNames []string `json:"commonNames"` } type UpdateSSLCertResponse struct { sdkResponseBase } func (c *Client) UpdateSSLCert(req *UpdateSSLCertRequest) (*UpdateSSLCertResponse, error) { return c.UpdateSSLCertWithContext(context.Background(), req) } func (c *Client) UpdateSSLCertWithContext(ctx context.Context, req *UpdateSSLCertRequest) (*UpdateSSLCertResponse, error) { if err := c.ensureAccessTokenExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodPost, "/SSLCertService/updateSSLCert") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &UpdateSSLCertResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/flexcdn/client.go ================================================ package flexcdn import ( "crypto/tls" "encoding/json" "fmt" "net/http" "net/url" "strings" "sync" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { apiRole string accessKeyId string accessKey string accessToken string accessTokenExp time.Time accessTokenMtx sync.Mutex client *resty.Client } func NewClient(serverUrl, apiRole, accessKeyId, accessKey string) (*Client, error) { if serverUrl == "" { return nil, fmt.Errorf("sdkerr: unset serverUrl") } if _, err := url.Parse(serverUrl); err != nil { return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err) } if apiRole == "" { return nil, fmt.Errorf("sdkerr: unset apiRole") } if apiRole != "user" && apiRole != "admin" { return nil, fmt.Errorf("sdkerr: invalid apiRole") } if accessKeyId == "" { return nil, fmt.Errorf("sdkerr: unset accessKeyId") } if accessKey == "" { return nil, fmt.Errorf("sdkerr: unset accessKey") } client := &Client{ apiRole: apiRole, accessKeyId: accessKeyId, accessKey: accessKey, } client.client = resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { if client.accessToken != "" { req.Header.Set("X-Cloud-Access-Token", client.accessToken) } return nil }) return client, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) SetTLSConfig(config *tls.Config) *Client { c.client.SetTLSClientConfig(config) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if tcode := res.GetCode(); tcode != 200 { return resp, fmt.Errorf("sdkerr: code='%d', message='%s'", tcode, res.GetMessage()) } } } return resp, nil } func (c *Client) ensureAccessTokenExists() error { c.accessTokenMtx.Lock() defer c.accessTokenMtx.Unlock() if c.accessToken != "" && c.accessTokenExp.After(time.Now()) { return nil } httpreq, err := c.newRequest(http.MethodPost, "/APIAccessTokenService/getAPIAccessToken") if err != nil { return err } else { httpreq.SetBody(map[string]string{ "type": c.apiRole, "accessKeyId": c.accessKeyId, "accessKey": c.accessKey, }) } type getAPIAccessTokenResponse struct { sdkResponseBase Data *struct { Token string `json:"token"` ExpiresAt int64 `json:"expiresAt"` } `json:"data,omitempty"` } result := &getAPIAccessTokenResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return err } else if code := result.GetCode(); code != 200 { return fmt.Errorf("sdkerr: failed to get flexcdn access token: code='%d', message='%s'", code, result.GetMessage()) } else { c.accessToken = result.Data.Token c.accessTokenExp = time.Unix(result.Data.ExpiresAt, 0) } return nil } ================================================ FILE: pkg/sdk3rd/flexcdn/types.go ================================================ package flexcdn type sdkResponse interface { GetCode() int GetMessage() string } type sdkResponseBase struct { Code int `json:"code"` Message string `json:"message"` } func (r *sdkResponseBase) GetCode() int { return r.Code } func (r *sdkResponseBase) GetMessage() string { return r.Message } var _ sdkResponse = (*sdkResponseBase)(nil) ================================================ FILE: pkg/sdk3rd/flyio/api_import_custom_certificate.go ================================================ package flyio import ( "context" "fmt" "net/http" "net/url" ) type ImportCustomCertificateRequest struct { AppName string `json:"-"` Hostname string `json:"hostname"` Fullchain string `json:"fullchain"` PrivateKey string `json:"private_key"` } type ImportCustomCertificateResponse struct { sdkResponseBase Hostname string `json:"hostname"` Configured bool `json:"configured"` Status string `json:"status"` Certificates []*struct { Source string `json:"source"` Status string `json:"status"` CreatedAt string `json:"created_at"` ExpiresAt string `json:"expires_at"` Issuer string `json:"issuer"` } `json:"certificates"` } func (c *Client) ImportCustomCertificate(req *ImportCustomCertificateRequest) (*ImportCustomCertificateResponse, error) { return c.ImportCustomCertificateWithContext(context.Background(), req) } func (c *Client) ImportCustomCertificateWithContext(ctx context.Context, req *ImportCustomCertificateRequest) (*ImportCustomCertificateResponse, error) { if req.AppName == "" { return nil, fmt.Errorf("sdkerr: unset appName") } path := fmt.Sprintf("/apps/%s/certificates/custom", url.PathEscape(req.AppName)) httpreq, err := c.newRequest(http.MethodPost, path) if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &ImportCustomCertificateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/flyio/client.go ================================================ package flyio import ( "encoding/json" "fmt" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { client *resty.Client } func NewClient(apiToken string) (*Client, error) { if apiToken == "" { return nil, fmt.Errorf("sdkerr: unset apiToken") } client := resty.New(). SetBaseURL("https://api.machines.dev/v1"). SetHeader("Accept", "application/json"). SetHeader("Authorization", "Bearer "+apiToken). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent) return &Client{client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } } return resp, nil } ================================================ FILE: pkg/sdk3rd/flyio/types.go ================================================ package flyio type sdkResponse interface { GetError() string } type sdkResponseBase struct { Error *string `json:"error,omitempty"` } func (r *sdkResponseBase) GetError() string { if r.Error == nil { return "" } return *r.Error } var _ sdkResponse = (*sdkResponseBase)(nil) ================================================ FILE: pkg/sdk3rd/gcore/endpoint.go ================================================ package common const BASE_URL = "https://api.gcore.com" ================================================ FILE: pkg/sdk3rd/gcore/signer.go ================================================ package common import ( "net/http" "github.com/G-Core/gcorelabscdn-go/gcore" ) type AuthRequestSigner struct { apiToken string } var _ gcore.RequestSigner = (*AuthRequestSigner)(nil) func NewAuthRequestSigner(apiToken string) *AuthRequestSigner { return &AuthRequestSigner{ apiToken: apiToken, } } func (s *AuthRequestSigner) Sign(req *http.Request) error { req.Header.Set("Authorization", "APIKey "+s.apiToken) return nil } ================================================ FILE: pkg/sdk3rd/gname/api_add_domain_resolution.go ================================================ package gname import ( "context" "encoding/json" "net/http" ) type AddDomainResolutionRequest struct { ZoneName *string `json:"ym,omitempty"` RecordType *string `json:"lx,omitempty"` RecordName *string `json:"zj,omitempty"` RecordValue *string `json:"jlz,omitempty"` MX *int32 `json:"mx,omitempty"` TTL *int32 `json:"ttl,omitempty"` } type AddDomainResolutionResponse struct { sdkResponseBase Data json.Number `json:"data"` } func (c *Client) AddDomainResolution(req *AddDomainResolutionRequest) (*AddDomainResolutionResponse, error) { return c.AddDomainResolutionWithContext(context.Background(), req) } func (c *Client) AddDomainResolutionWithContext(ctx context.Context, req *AddDomainResolutionRequest) (*AddDomainResolutionResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/api/resolution/add", req) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &AddDomainResolutionResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/gname/api_delete_domain_resolution.go ================================================ package gname import ( "context" "net/http" ) type DeleteDomainResolutionRequest struct { ZoneName *string `json:"ym,omitempty"` RecordID *int64 `json:"jxid,omitempty"` } type DeleteDomainResolutionResponse struct { sdkResponseBase } func (c *Client) DeleteDomainResolution(req *DeleteDomainResolutionRequest) (*DeleteDomainResolutionResponse, error) { return c.DeleteDomainResolutionWithContext(context.Background(), req) } func (c *Client) DeleteDomainResolutionWithContext(ctx context.Context, req *DeleteDomainResolutionRequest) (*DeleteDomainResolutionResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/api/resolution/delete", req) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &DeleteDomainResolutionResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/gname/api_list_domain_resolution.go ================================================ package gname import ( "context" "net/http" ) type ListDomainResolutionRequest struct { ZoneName *string `json:"ym,omitempty"` Page *int32 `json:"page,omitempty"` PageSize *int32 `json:"limit,omitempty"` } type ListDomainResolutionResponse struct { sdkResponseBase Count int32 `json:"count"` Data []*DomainResolutionRecordord `json:"data"` Page int32 `json:"page"` PageSize int32 `json:"pagesize"` } func (c *Client) ListDomainResolution(req *ListDomainResolutionRequest) (*ListDomainResolutionResponse, error) { return c.ListDomainResolutionWithContext(context.Background(), req) } func (c *Client) ListDomainResolutionWithContext(ctx context.Context, req *ListDomainResolutionRequest) (*ListDomainResolutionResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/api/resolution/list", req) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &ListDomainResolutionResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/gname/api_modify_domain_resolution.go ================================================ package gname import ( "context" "net/http" ) type ModifyDomainResolutionRequest struct { ID *int64 `json:"jxid,omitempty"` ZoneName *string `json:"ym,omitempty"` RecordType *string `json:"lx,omitempty"` RecordName *string `json:"zj,omitempty"` RecordValue *string `json:"jlz,omitempty"` MX *int32 `json:"mx,omitempty"` TTL *int32 `json:"ttl,omitempty"` } type ModifyDomainResolutionResponse struct { sdkResponseBase } func (c *Client) ModifyDomainResolution(req *ModifyDomainResolutionRequest) (*ModifyDomainResolutionResponse, error) { return c.ModifyDomainResolutionWithContext(context.Background(), req) } func (c *Client) ModifyDomainResolutionWithContext(ctx context.Context, req *ModifyDomainResolutionRequest) (*ModifyDomainResolutionResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/api/resolution/edit", req) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &ModifyDomainResolutionResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/gname/client.go ================================================ package gname import ( "crypto/md5" "encoding/json" "fmt" "net/url" "sort" "strings" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { appId string appKey string client *resty.Client } func NewClient(appId, appKey string) (*Client, error) { if appId == "" { return nil, fmt.Errorf("sdkerr: unset appId") } if appKey == "" { return nil, fmt.Errorf("sdkerr: unset appKey") } client := resty.New(). SetBaseURL("https://api.gname.com"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/x-www-form-urlencoded"). SetHeader("User-Agent", app.AppUserAgent) return &Client{ appId: appId, appKey: appKey, client: client, }, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string, params any) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } data := make(map[string]string) if params != nil { temp := make(map[string]any) jsonb, _ := json.Marshal(params) json.Unmarshal(jsonb, &temp) for k, v := range temp { if v == nil { continue } data[k] = fmt.Sprintf("%v", v) } } data["appid"] = c.appId data["gntime"] = fmt.Sprintf("%d", time.Now().Unix()) data["gntoken"] = generateSignature(data, c.appKey) req := c.client.R() req.Method = method req.URL = path req.SetFormData(data) return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetBody` or `req.SetFormData` HERE! USE `newRequest` INSTEAD. // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if tcode := res.GetCode(); tcode != 1 { return resp, fmt.Errorf("sdkerr: api error: code='%d', message='%s'", tcode, res.GetMessage()) } } } return resp, nil } func generateSignature(params map[string]string, appKey string) string { // Step 1: Sort parameters by ASCII order var keys []string for k := range params { keys = append(keys, k) } sort.Strings(keys) // Step 2: Create string A with URL-encoded values var pairs []string for _, k := range keys { encodedValue := url.QueryEscape(params[k]) pairs = append(pairs, fmt.Sprintf("%s=%s", k, encodedValue)) } stringA := strings.Join(pairs, "&") // Step 3: Append appkey to create string B stringB := stringA + appKey // Step 4: Calculate MD5 and convert to uppercase hash := md5.Sum([]byte(stringB)) return strings.ToUpper(fmt.Sprintf("%x", hash)) } ================================================ FILE: pkg/sdk3rd/gname/types.go ================================================ package gname import "encoding/json" type sdkResponse interface { GetCode() int GetMessage() string } type sdkResponseBase struct { Code int `json:"code"` Message string `json:"msg"` } func (r *sdkResponseBase) GetCode() int { return r.Code } func (r *sdkResponseBase) GetMessage() string { return r.Message } var _ sdkResponse = (*sdkResponseBase)(nil) type DomainResolutionRecordord struct { ID json.Number `json:"id"` ZoneName string `json:"ym"` RecordType string `json:"lx"` RecordName string `json:"zjt"` RecordValue string `json:"jxz"` MX int32 `json:"mx"` } ================================================ FILE: pkg/sdk3rd/goedge/api_update_ssl_cert.go ================================================ package goedge import ( "context" "net/http" ) type UpdateSSLCertRequest struct { SSLCertId int64 `json:"sslCertId"` IsOn bool `json:"isOn"` Name string `json:"name"` Description string `json:"description"` ServerName string `json:"serverName"` IsCA bool `json:"isCA"` CertData string `json:"certData"` KeyData string `json:"keyData"` TimeBeginAt int64 `json:"timeBeginAt"` TimeEndAt int64 `json:"timeEndAt"` DNSNames []string `json:"dnsNames"` CommonNames []string `json:"commonNames"` } type UpdateSSLCertResponse struct { sdkResponseBase } func (c *Client) UpdateSSLCert(req *UpdateSSLCertRequest) (*UpdateSSLCertResponse, error) { return c.UpdateSSLCertWithContext(context.Background(), req) } func (c *Client) UpdateSSLCertWithContext(ctx context.Context, req *UpdateSSLCertRequest) (*UpdateSSLCertResponse, error) { if err := c.ensureAccessTokenExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodPost, "/SSLCertService/updateSSLCert") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &UpdateSSLCertResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/goedge/client.go ================================================ package goedge import ( "crypto/tls" "encoding/json" "fmt" "net/http" "net/url" "strings" "sync" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { apiRole string accessKeyId string accessKey string accessToken string accessTokenExp time.Time accessTokenMtx sync.Mutex client *resty.Client } func NewClient(serverUrl, apiRole, accessKeyId, accessKey string) (*Client, error) { if serverUrl == "" { return nil, fmt.Errorf("sdkerr: unset serverUrl") } if _, err := url.Parse(serverUrl); err != nil { return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err) } if apiRole == "" { return nil, fmt.Errorf("sdkerr: unset apiRole") } if apiRole != "user" && apiRole != "admin" { return nil, fmt.Errorf("sdkerr: invalid apiRole") } if accessKeyId == "" { return nil, fmt.Errorf("sdkerr: unset accessKeyId") } if accessKey == "" { return nil, fmt.Errorf("sdkerr: unset accessKey") } client := &Client{ apiRole: apiRole, accessKeyId: accessKeyId, accessKey: accessKey, } client.client = resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { if client.accessToken != "" { req.Header.Set("X-Edge-Access-Token", client.accessToken) } return nil }) return client, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) SetTLSConfig(config *tls.Config) *Client { c.client.SetTLSClientConfig(config) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if tcode := res.GetCode(); tcode != 200 { return resp, fmt.Errorf("sdkerr: code='%d', message='%s'", tcode, res.GetMessage()) } } } return resp, nil } func (c *Client) ensureAccessTokenExists() error { c.accessTokenMtx.Lock() defer c.accessTokenMtx.Unlock() if c.accessToken != "" && c.accessTokenExp.After(time.Now()) { return nil } httpreq, err := c.newRequest(http.MethodPost, "/APIAccessTokenService/getAPIAccessToken") if err != nil { return err } else { httpreq.SetBody(map[string]string{ "type": c.apiRole, "accessKeyId": c.accessKeyId, "accessKey": c.accessKey, }) } type getAPIAccessTokenResponse struct { sdkResponseBase Data *struct { Token string `json:"token"` ExpiresAt int64 `json:"expiresAt"` } `json:"data,omitempty"` } result := &getAPIAccessTokenResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return err } else if code := result.GetCode(); code != 200 { return fmt.Errorf("sdkerr: failed to get goedge access token: code='%d', message='%s'", code, result.GetMessage()) } else { c.accessToken = result.Data.Token c.accessTokenExp = time.Unix(result.Data.ExpiresAt, 0) } return nil } ================================================ FILE: pkg/sdk3rd/goedge/types.go ================================================ package goedge type sdkResponse interface { GetCode() int GetMessage() string } type sdkResponseBase struct { Code int `json:"code"` Message string `json:"message"` } func (r *sdkResponseBase) GetCode() int { return r.Code } func (r *sdkResponseBase) GetMessage() string { return r.Message } var _ sdkResponse = (*sdkResponseBase)(nil) ================================================ FILE: pkg/sdk3rd/lecdn/v3/client/api_update_certificate.go ================================================ package client import ( "context" "fmt" "net/http" ) type UpdateCertificateRequest struct { Name string `json:"name"` Description string `json:"description"` Type string `json:"type"` SSLPEM string `json:"ssl_pem"` SSLKey string `json:"ssl_key"` AutoRenewal bool `json:"auto_renewal"` } type UpdateCertificateResponse struct { sdkResponseBase } func (c *Client) UpdateCertificate(certId int64, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) { return c.UpdateCertificateWithContext(context.Background(), certId, req) } func (c *Client) UpdateCertificateWithContext(ctx context.Context, certId int64, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) { if certId == 0 { return nil, fmt.Errorf("sdkerr: unset certId") } if err := c.ensureAccessTokenExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf("/certificate/%d", certId)) if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &UpdateCertificateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/lecdn/v3/client/client.go ================================================ package client import ( "crypto/tls" "encoding/json" "fmt" "net/http" "net/url" "strings" "sync" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { username string password string accessToken string accessTokenMtx sync.Mutex client *resty.Client } func NewClient(serverUrl, username, password string) (*Client, error) { if serverUrl == "" { return nil, fmt.Errorf("sdkerr: unset serverUrl") } if _, err := url.Parse(serverUrl); err != nil { return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err) } if username == "" { return nil, fmt.Errorf("sdkerr: unset username") } if password == "" { return nil, fmt.Errorf("sdkerr: unset password") } client := &Client{ username: username, password: password, } client.client = resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")+"/prod-api"). SetHeader("User-Agent", app.AppUserAgent). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { if client.accessToken != "" { req.Header.Set("Authorization", "Bearer "+client.accessToken) } return nil }) return client, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) SetTLSConfig(config *tls.Config) *Client { c.client.SetTLSClientConfig(config) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if tcode := res.GetCode(); tcode != 200 { return resp, fmt.Errorf("sdkerr: code='%d', message='%s'", tcode, res.GetMessage()) } } } return resp, nil } func (c *Client) ensureAccessTokenExists() error { c.accessTokenMtx.Lock() defer c.accessTokenMtx.Unlock() if c.accessToken != "" { return nil } httpreq, err := c.newRequest(http.MethodPost, "/auth/login") if err != nil { return err } else { httpreq.SetBody(map[string]string{ "email": c.username, "username": c.username, "password": c.password, }) } type loginResponse struct { sdkResponseBase Data *struct { UserId int64 `json:"user_id"` Username string `json:"username"` Token string `json:"token"` } `json:"data,omitempty"` } result := &loginResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return err } else { c.accessToken = result.Data.Token } return nil } ================================================ FILE: pkg/sdk3rd/lecdn/v3/client/types.go ================================================ package client type sdkResponse interface { GetCode() int GetMessage() string } type sdkResponseBase struct { Code int `json:"code"` Message string `json:"msg"` } func (r *sdkResponseBase) GetCode() int { return r.Code } func (r *sdkResponseBase) GetMessage() string { return r.Message } var _ sdkResponse = (*sdkResponseBase)(nil) ================================================ FILE: pkg/sdk3rd/lecdn/v3/master/api_update_certificate.go ================================================ package master import ( "context" "fmt" "net/http" ) type UpdateCertificateRequest struct { ClientId int64 `json:"client_id"` Name string `json:"name"` Description string `json:"description"` Type string `json:"type"` SSLPEM string `json:"ssl_pem"` SSLKey string `json:"ssl_key"` AutoRenewal bool `json:"auto_renewal"` } type UpdateCertificateResponse struct { sdkResponseBase } func (c *Client) UpdateCertificate(certId int64, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) { return c.UpdateCertificateWithContext(context.Background(), certId, req) } func (c *Client) UpdateCertificateWithContext(ctx context.Context, certId int64, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) { if certId == 0 { return nil, fmt.Errorf("sdkerr: unset certId") } if err := c.ensureAccessTokenExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf("/certificate/%d", certId)) if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &UpdateCertificateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/lecdn/v3/master/client.go ================================================ package master import ( "crypto/tls" "encoding/json" "fmt" "net/http" "net/url" "strings" "sync" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { username string password string accessToken string accessTokenMtx sync.Mutex client *resty.Client } func NewClient(serverUrl, username, password string) (*Client, error) { if serverUrl == "" { return nil, fmt.Errorf("sdkerr: unset serverUrl") } if _, err := url.Parse(serverUrl); err != nil { return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err) } if username == "" { return nil, fmt.Errorf("sdkerr: unset username") } if password == "" { return nil, fmt.Errorf("sdkerr: unset password") } client := &Client{ username: username, password: password, } client.client = resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")+"/prod-api"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { if client.accessToken != "" { req.Header.Set("Authorization", "Bearer "+client.accessToken) } return nil }) return client, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) SetTLSConfig(config *tls.Config) *Client { c.client.SetTLSClientConfig(config) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if tcode := res.GetCode(); tcode != 200 { return resp, fmt.Errorf("sdkerr: code='%d', message='%s'", tcode, res.GetMessage()) } } } return resp, nil } func (c *Client) ensureAccessTokenExists() error { c.accessTokenMtx.Lock() defer c.accessTokenMtx.Unlock() if c.accessToken != "" { return nil } httpreq, err := c.newRequest(http.MethodPost, "/auth/login") if err != nil { return err } else { httpreq.SetBody(map[string]string{ "username": c.username, "password": c.password, }) } type loginResponse struct { sdkResponseBase Data *struct { UserId int64 `json:"user_id"` Username string `json:"username"` Token string `json:"token"` } `json:"data,omitempty"` } result := &loginResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return err } else { c.accessToken = result.Data.Token } return nil } ================================================ FILE: pkg/sdk3rd/lecdn/v3/master/types.go ================================================ package master type sdkResponse interface { GetCode() int GetMessage() string } type sdkResponseBase struct { Code int `json:"code"` Message string `json:"message"` } func (r *sdkResponseBase) GetCode() int { return r.Code } func (r *sdkResponseBase) GetMessage() string { return r.Message } var _ sdkResponse = (*sdkResponseBase)(nil) ================================================ FILE: pkg/sdk3rd/netlify/api_provision_site_tls_certificate.go ================================================ package netlify import ( "context" "fmt" "net/http" "net/url" ) type ProvisionSiteTLSCertificateParams struct { Certificate string `json:"certificate"` CACertificates string `json:"ca_certificates"` Key string `json:"key"` } type ProvisionSiteTLSCertificateResponse struct { sdkResponseBase Domains []string `json:"domains,omitempty"` State string `json:"state,omitempty"` ExpiresAt string `json:"expires_at,omitempty"` CreatedAt string `json:"created_at,omitempty"` UpdatedAt string `json:"updated_at,omitempty"` } func (c *Client) ProvisionSiteTLSCertificate(siteId string, req *ProvisionSiteTLSCertificateParams) (*ProvisionSiteTLSCertificateResponse, error) { return c.ProvisionSiteTLSCertificateWithContext(context.Background(), siteId, req) } func (c *Client) ProvisionSiteTLSCertificateWithContext(ctx context.Context, siteId string, req *ProvisionSiteTLSCertificateParams) (*ProvisionSiteTLSCertificateResponse, error) { if siteId == "" { return nil, fmt.Errorf("sdkerr: unset siteId") } httpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf("/sites/%s/ssl", url.PathEscape(siteId))) if err != nil { return nil, err } else { httpreq.SetQueryParams(map[string]string{ "certificate": req.Certificate, "ca_certificates": req.CACertificates, "key": req.Key, }) httpreq.SetContext(ctx) } result := &ProvisionSiteTLSCertificateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/netlify/client.go ================================================ package netlify import ( "encoding/json" "fmt" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { client *resty.Client } func NewClient(apiToken string) (*Client, error) { if apiToken == "" { return nil, fmt.Errorf("sdkerr: unset apiToken") } client := resty.New(). SetBaseURL("https://api.netlify.com/api/v1"). SetHeader("Accept", "application/json"). SetHeader("Authorization", "Bearer "+apiToken). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent) return &Client{client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if tcode := res.GetCode(); tcode != 0 { return resp, fmt.Errorf("sdkerr: code='%d', message='%s'", tcode, res.GetMessage()) } } } return resp, nil } ================================================ FILE: pkg/sdk3rd/netlify/types.go ================================================ package netlify type sdkResponse interface { GetCode() int GetMessage() string } type sdkResponseBase struct { Code *int `json:"code,omitempty"` Message *string `json:"message,omitempty"` } func (r *sdkResponseBase) GetCode() int { if r.Code == nil { return 0 } return *r.Code } func (r *sdkResponseBase) GetMessage() string { if r.Message == nil { return "" } return *r.Message } var _ sdkResponse = (*sdkResponseBase)(nil) ================================================ FILE: pkg/sdk3rd/nginxproxymanager/api_nginx_create_certificate.go ================================================ package nginxproxymanager import ( "context" "net/http" ) type NginxCreateCertificateRequest struct { Provider string `json:"provider"` NiceName string `json:"nice_name"` } type NginxCreateCertificateResponse struct { CertificateRecord } func (c *Client) NginxCreateCertificate(req *NginxCreateCertificateRequest) (*NginxCreateCertificateResponse, error) { return c.NginxCreateCertificateWithContext(context.Background(), req) } func (c *Client) NginxCreateCertificateWithContext(ctx context.Context, req *NginxCreateCertificateRequest) (*NginxCreateCertificateResponse, error) { if err := c.ensureJwtTokenExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodPost, "/nginx/certificates") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &NginxCreateCertificateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/nginxproxymanager/api_nginx_list_certificates.go ================================================ package nginxproxymanager import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type NginxListCertificatesRequest struct { Expand *string `json:"expand,omitempty" url:"expand,omitempty"` } type NginxListCertificatesResponse = []*CertificateRecord func (c *Client) NginxListCertificates(req *NginxListCertificatesRequest) (*NginxListCertificatesResponse, error) { return c.NginxListCertificatesWithContext(context.Background(), req) } func (c *Client) NginxListCertificatesWithContext(ctx context.Context, req *NginxListCertificatesRequest) (*NginxListCertificatesResponse, error) { if err := c.ensureJwtTokenExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodGet, "/nginx/certificates") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &NginxListCertificatesResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/nginxproxymanager/api_nginx_list_dead_hosts.go ================================================ package nginxproxymanager import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type NginxListDeadHostsRequest struct { Expand *string `json:"expand,omitempty" url:"expand,omitempty"` } type NginxListDeadHostsResponse = []*DeadHostRecord func (c *Client) NginxListDeadHosts(req *NginxListDeadHostsRequest) (*NginxListDeadHostsResponse, error) { return c.NginxListDeadHostsWithContext(context.Background(), req) } func (c *Client) NginxListDeadHostsWithContext(ctx context.Context, req *NginxListDeadHostsRequest) (*NginxListDeadHostsResponse, error) { if err := c.ensureJwtTokenExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodGet, "/nginx/dead-hosts") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &NginxListDeadHostsResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/nginxproxymanager/api_nginx_list_proxy_hosts.go ================================================ package nginxproxymanager import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type NginxListProxyHostsRequest struct { Expand *string `json:"expand,omitempty" url:"expand,omitempty"` } type NginxListProxyHostsResponse = []*ProxyHostRecord func (c *Client) NginxListProxyHosts(req *NginxListProxyHostsRequest) (*NginxListProxyHostsResponse, error) { return c.NginxListProxyHostsWithContext(context.Background(), req) } func (c *Client) NginxListProxyHostsWithContext(ctx context.Context, req *NginxListProxyHostsRequest) (*NginxListProxyHostsResponse, error) { if err := c.ensureJwtTokenExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodGet, "/nginx/proxy-hosts") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &NginxListProxyHostsResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/nginxproxymanager/api_nginx_list_redirection_hosts.go ================================================ package nginxproxymanager import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type NginxListRedirectionHostsRequest struct { Expand *string `json:"expand,omitempty" url:"expand,omitempty"` } type NginxListRedirectionHostsResponse = []*RedirectionHostRecord func (c *Client) NginxListRedirectionHosts(req *NginxListRedirectionHostsRequest) (*NginxListRedirectionHostsResponse, error) { return c.NginxListRedirectionHostsWithContext(context.Background(), req) } func (c *Client) NginxListRedirectionHostsWithContext(ctx context.Context, req *NginxListRedirectionHostsRequest) (*NginxListRedirectionHostsResponse, error) { if err := c.ensureJwtTokenExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodGet, "/nginx/redirection-hosts") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &NginxListRedirectionHostsResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/nginxproxymanager/api_nginx_list_streams.go ================================================ package nginxproxymanager import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type NginxListStreamsRequest struct { Expand *string `json:"expand,omitempty" url:"expand,omitempty"` } type NginxListStreamsResponse = []*StreamHostRecord func (c *Client) NginxListStreams(req *NginxListStreamsRequest) (*NginxListStreamsResponse, error) { return c.NginxListStreamsWithContext(context.Background(), req) } func (c *Client) NginxListStreamsWithContext(ctx context.Context, req *NginxListStreamsRequest) (*NginxListStreamsResponse, error) { if err := c.ensureJwtTokenExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodGet, "/nginx/streams") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &NginxListStreamsResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/nginxproxymanager/api_nginx_update_dead_host.go ================================================ package nginxproxymanager import ( "context" "fmt" "net/http" ) type NginxUpdateDeadHostRequest struct { CertificateId *int64 `json:"certificate_id,omitempty"` } type NginxUpdateDeadHostResponse struct { DeadHostRecord } func (c *Client) NginxUpdateDeadHost(hostId int64, req *NginxUpdateDeadHostRequest) (*NginxUpdateDeadHostResponse, error) { return c.NginxUpdateDeadHostWithContext(context.Background(), hostId, req) } func (c *Client) NginxUpdateDeadHostWithContext(ctx context.Context, hostId int64, req *NginxUpdateDeadHostRequest) (*NginxUpdateDeadHostResponse, error) { if hostId == 0 { return nil, fmt.Errorf("sdkerr: unset hostId") } if err := c.ensureJwtTokenExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf("/nginx/dead-hosts/%d", hostId)) if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &NginxUpdateDeadHostResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/nginxproxymanager/api_nginx_update_proxy_host.go ================================================ package nginxproxymanager import ( "context" "fmt" "net/http" ) type NginxUpdateProxyHostRequest struct { CertificateId *int64 `json:"certificate_id,omitempty"` } type NginxUpdateProxyHostResponse struct { ProxyHostRecord } func (c *Client) NginxUpdateProxyHost(hostId int64, req *NginxUpdateProxyHostRequest) (*NginxUpdateProxyHostResponse, error) { return c.NginxUpdateProxyHostWithContext(context.Background(), hostId, req) } func (c *Client) NginxUpdateProxyHostWithContext(ctx context.Context, hostId int64, req *NginxUpdateProxyHostRequest) (*NginxUpdateProxyHostResponse, error) { if hostId == 0 { return nil, fmt.Errorf("sdkerr: unset hostId") } if err := c.ensureJwtTokenExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf("/nginx/proxy-hosts/%d", hostId)) if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &NginxUpdateProxyHostResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/nginxproxymanager/api_nginx_update_redirection_host.go ================================================ package nginxproxymanager import ( "context" "fmt" "net/http" ) type NginxUpdateRedirectionHostRequest struct { CertificateId *int64 `json:"certificate_id,omitempty"` } type NginxUpdateRedirectionHostResponse struct { RedirectionHostRecord } func (c *Client) NginxUpdateRedirectionHost(hostId int64, req *NginxUpdateRedirectionHostRequest) (*NginxUpdateRedirectionHostResponse, error) { return c.NginxUpdateRedirectionHostWithContext(context.Background(), hostId, req) } func (c *Client) NginxUpdateRedirectionHostWithContext(ctx context.Context, hostId int64, req *NginxUpdateRedirectionHostRequest) (*NginxUpdateRedirectionHostResponse, error) { if hostId == 0 { return nil, fmt.Errorf("sdkerr: unset hostId") } if err := c.ensureJwtTokenExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf("/nginx/redirection-hosts/%d", hostId)) if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &NginxUpdateRedirectionHostResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/nginxproxymanager/api_nginx_update_stream.go ================================================ package nginxproxymanager import ( "context" "fmt" "net/http" ) type NginxUpdateStreamRequest struct { CertificateId *int64 `json:"certificate_id,omitempty"` } type NginxUpdateStreamResponse struct { StreamHostRecord } func (c *Client) NginxUpdateStream(hostId int64, req *NginxUpdateStreamRequest) (*NginxUpdateStreamResponse, error) { return c.NginxUpdateStreamWithContext(context.Background(), hostId, req) } func (c *Client) NginxUpdateStreamWithContext(ctx context.Context, hostId int64, req *NginxUpdateStreamRequest) (*NginxUpdateStreamResponse, error) { if hostId == 0 { return nil, fmt.Errorf("sdkerr: unset hostId") } if err := c.ensureJwtTokenExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf("/nginx/streams/%d", hostId)) if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &NginxUpdateStreamResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/nginxproxymanager/api_nginx_upload_certificate.go ================================================ package nginxproxymanager import ( "context" "fmt" "net/http" "strings" ) type NginxUploadCertificateRequest struct { CertificateMeta } type NginxUploadCertificateResponse struct { CertificateMeta } func (c *Client) NginxUploadCertificate(certId int64, req *NginxUploadCertificateRequest) (*NginxUploadCertificateResponse, error) { return c.NginxUploadCertificateWithContext(context.Background(), certId, req) } func (c *Client) NginxUploadCertificateWithContext(ctx context.Context, certId int64, req *NginxUploadCertificateRequest) (*NginxUploadCertificateResponse, error) { if certId == 0 { return nil, fmt.Errorf("sdkerr: unset certId") } if err := c.ensureJwtTokenExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf("/nginx/certificates/%d/upload", certId)) if err != nil { return nil, err } else { httpreq.SetFileReader("certificate", "certificate.pem", strings.NewReader(req.Certificate)) httpreq.SetFileReader("certificate_key", "privkey.pem", strings.NewReader(req.CertificateKey)) httpreq.SetFileReader("intermediate_certificate", "cabundle.pem", strings.NewReader(req.IntermediateCertificate)) httpreq.SetContext(ctx) } result := &NginxUploadCertificateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/nginxproxymanager/api_settings_get_default_site.go ================================================ package nginxproxymanager import ( "context" "net/http" ) type SettingsGetDefaultSiteRequest struct{} type SettingsGetDefaultSiteResponse struct { Id string `json:"id"` Name string `json:"name"` Description string `json:"description"` Value string `json:"value"` Meta struct { Redirect string `json:"redirect"` Html string `json:"urhtmll"` } `json:"meta"` } func (c *Client) SettingsGetDefaultSite(req *SettingsGetDefaultSiteRequest) (*SettingsGetDefaultSiteResponse, error) { return c.SettingsGetDefaultSiteWithContext(context.Background(), req) } func (c *Client) SettingsGetDefaultSiteWithContext(ctx context.Context, req *SettingsGetDefaultSiteRequest) (*SettingsGetDefaultSiteResponse, error) { if err := c.ensureJwtTokenExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodGet, "/settings/default-site") if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &SettingsGetDefaultSiteResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/nginxproxymanager/api_settings_set_default_site.go ================================================ package nginxproxymanager import ( "context" "net/http" ) type SettingsSetDefaultSiteRequest struct { Value string `json:"value"` Meta struct { Redirect string `json:"redirect"` Html string `json:"html"` } `json:"meta"` } type SettingsSetDefaultSiteResponse struct{} func (c *Client) SettingsSetDefaultSite(req *SettingsSetDefaultSiteRequest) (*SettingsSetDefaultSiteResponse, error) { return c.SettingsSetDefaultSiteWithContext(context.Background(), req) } func (c *Client) SettingsSetDefaultSiteWithContext(ctx context.Context, req *SettingsSetDefaultSiteRequest) (*SettingsSetDefaultSiteResponse, error) { if err := c.ensureJwtTokenExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodPut, "/settings/default-site") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &SettingsSetDefaultSiteResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/nginxproxymanager/client.go ================================================ package nginxproxymanager import ( "crypto/tls" "encoding/json" "fmt" "net/http" "net/url" "strings" "sync" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { identity string secret string jwtToken string jwtTokenMtx sync.Mutex client *resty.Client } func NewClient(serverUrl, identity, secret string) (*Client, error) { if serverUrl == "" { return nil, fmt.Errorf("sdkerr: unset serverUrl") } if _, err := url.Parse(serverUrl); err != nil { return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err) } if identity == "" { return nil, fmt.Errorf("sdkerr: unset identity") } if secret == "" { return nil, fmt.Errorf("sdkerr: unset secret") } client := &Client{ identity: identity, secret: secret, } client.client = resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")+"/api"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { if client.jwtToken != "" { req.Header.Set("Authorization", "Bearer "+client.jwtToken) } return nil }) return client, nil } func NewClientWithJwtToken(serverUrl, jwtToken string) (*Client, error) { if serverUrl == "" { return nil, fmt.Errorf("sdkerr: unset serverUrl") } if _, err := url.Parse(serverUrl); err != nil { return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err) } if jwtToken == "" { return nil, fmt.Errorf("sdkerr: unset jwtToken") } client := &Client{ jwtToken: jwtToken, } client.client = resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")+"/api"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { if client.jwtToken != "" { req.Header.Set("Authorization", "Bearer "+client.jwtToken) } return nil }) return client, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) SetTLSConfig(config *tls.Config) *Client { c.client.SetTLSClientConfig(config) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res interface{}) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { var errRes *sdkResponseBase if err := json.Unmarshal(resp.Body(), &errRes); err == nil { if terror := errRes.GetError(); terror != "" { return resp, fmt.Errorf("sdkerr: error='%s'", terror) } } if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } } return resp, nil } func (c *Client) ensureJwtTokenExists() error { c.jwtTokenMtx.Lock() defer c.jwtTokenMtx.Unlock() if c.jwtToken != "" { return nil } httpreq, err := c.newRequest(http.MethodPost, "/tokens") if err != nil { return err } else { httpreq.SetBody(map[string]string{ "identity": c.identity, "secret": c.secret, }) } type tokensResponse struct { sdkResponseBase Token string `json:"token"` Expires string `json:"expires"` } result := &tokensResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return err } else if terror := result.GetError(); terror != "" { return fmt.Errorf("sdkerr: failed to create npm token: error='%s'", terror) } else { c.jwtToken = result.Token } return nil } ================================================ FILE: pkg/sdk3rd/nginxproxymanager/types.go ================================================ package nginxproxymanager import ( "encoding/json" "fmt" ) type sdkResponse interface { GetError() string } type sdkResponseBase struct { Error json.RawMessage `json:"error"` } func (r *sdkResponseBase) GetError() string { if len(r.Error) == 0 { return "" } var errStr string if err := json.Unmarshal(r.Error, &errStr); err == nil { return errStr } type errObjType struct { Code int `json:"code"` Message string `json:"message"` } var errObj errObjType if err := json.Unmarshal(r.Error, &errObj); err == nil && errObj.Message != "" { if errObj.Code != 0 { return fmt.Sprintf("%d %s", errObj.Code, errObj.Message) } return errObj.Message } var errMap map[string]interface{} if err := json.Unmarshal(r.Error, &errMap); err == nil { if message, ok := errMap["message"].(string); ok { return message } } return "" } var _ sdkResponse = (*sdkResponseBase)(nil) type CertificateRecord struct { Id int64 `json:"id"` CreatedOn string `json:"created_on"` ModifiedOn string `json:"modified_on"` Provider string `json:"provider"` NiceName string `json:"nice_name"` DomainNames []string `json:"domain_names"` ExpiresOn string `json:"expires_on"` Meta CertificateMeta `json:"meta"` } type CertificateMeta struct { Certificate string `json:"certificate"` CertificateKey string `json:"certificate_key"` IntermediateCertificate string `json:"intermediate_certificate"` } type HostRecord struct { Id int64 `json:"id"` CreatedOn string `json:"created_on"` ModifiedOn string `json:"modified_on"` DomainNames []string `json:"domain_names"` CertificateId int64 `json:"certificate_id"` Meta HostMeta `json:"meta"` Enabled bool `json:"enabled"` } type HostMeta struct { NginxOnline bool `json:"nginx_online"` NginxErr any `json:"nginx_err"` } type ProxyHostRecord struct { HostRecord ForwardScheme string `json:"forward_scheme"` ForwardHost string `json:"forward_host"` ForwardPort int32 `json:"forward_port"` SslForced bool `json:"ssl_forced"` Http2Support bool `json:"http2_support"` HstsEnabled bool `json:"hsts_enabled"` HstsSubdomains bool `json:"hsts_subdomains"` } type RedirectionHostRecord struct { HostRecord ForwardScheme string `json:"forward_scheme"` ForwardDomainName string `json:"forward_domain_name"` ForwardHttpCode int32 `json:"forward_http_code"` SslForced bool `json:"ssl_forced"` Http2Support bool `json:"http2_support"` HstsEnabled bool `json:"hsts_enabled"` HstsSubdomains bool `json:"hsts_subdomains"` } type StreamHostRecord struct { HostRecord ForwardingHost string `json:"forwarding_host"` ForwardingPort int32 `json:"forwarding_port"` IncomingPort int32 `json:"incoming_port"` TcpForwarding bool `json:"tcp_forwarding"` UdpForwarding bool `json:"udp_forwarding"` } type DeadHostRecord struct { HostRecord SslForced bool `json:"ssl_forced"` Http2Support bool `json:"http2_support"` HstsEnabled bool `json:"hsts_enabled"` HstsSubdomains bool `json:"hsts_subdomains"` } ================================================ FILE: pkg/sdk3rd/qingcloud/dns/api_create_record.go ================================================ package dns import ( "context" "net/http" ) type CreateRecordRequest struct { ZoneName *string `json:"zone_name,omitempty"` DomainName *string `json:"domain_name,omitempty"` ViewId *int32 `json:"view_id,omitempty"` Type *string `json:"type,omitempty"` Records []*CreateRecordRequestRecord `json:"record,omitempty"` Ttl *int32 `json:"ttl,omitempty"` Mode *int32 `json:"mode,omitempty"` AutoMerge *int32 `json:"auto_merge,omitempty"` } type CreateRecordRequestRecord struct { Values []*CreateRecordRequestRecordValue `json:"values,omitempty"` Weight *int32 `json:"weight,omitempty"` } type CreateRecordRequestRecordValue struct { Value *string `json:"value,omitempty"` Status *int32 `json:"status,omitempty"` } type CreateRecordResponse struct { sdkResponseBase DomainName *string `json:"domain_name,omitempty"` DomainRecordId *int64 `json:"domain_record_id,omitempty"` ViewId *int64 `json:"view_id,omitempty"` Records []*CreateRecordResponseRecord `json:"records,omitempty"` } type CreateRecordResponseRecord struct { GroupId *int64 `json:"group_id,omitempty"` GroupStatus *int32 `json:"group_status,omitempty"` Values []*CreateRecordResponseRecordValue `json:"value,omitempty"` Weight *int32 `json:"weight,omitempty"` } type CreateRecordResponseRecordValue struct { ValueId *int64 `json:"id,omitempty"` Value *string `json:"value,omitempty"` Status *int32 `json:"status,omitempty"` } func (c *Client) CreateRecord(req *CreateRecordRequest) (*CreateRecordResponse, error) { return c.CreateRecordWithContext(context.Background(), req) } func (c *Client) CreateRecordWithContext(ctx context.Context, req *CreateRecordRequest) (*CreateRecordResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/v1/record/") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &CreateRecordResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/qingcloud/dns/api_delete_record.go ================================================ package dns import ( "context" "fmt" "net/http" ) type DeleteRecordResponse struct { sdkResponseBase } func (c *Client) DeleteRecord(recordIds []*int64) (*DeleteRecordResponse, error) { return c.DeleteRecordWithContext(context.Background(), recordIds) } func (c *Client) DeleteRecordWithContext(ctx context.Context, recordIds []*int64) (*DeleteRecordResponse, error) { if len(recordIds) == 0 { return nil, fmt.Errorf("sdkerr: unset recordIds") } httpreq, err := c.newRequest(http.MethodPost, "/v1/change_record_status/") if err != nil { return nil, err } else { httpreq.SetBody(map[string]any{ "ids": recordIds, "action": "delete", "target": "record", }) httpreq.SetContext(ctx) } result := &DeleteRecordResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/qingcloud/dns/client.go ================================================ package dns import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "net/http" "net/url" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { client *resty.Client } func NewClient(accessKeyId, secretAccessKey string) (*Client, error) { if accessKeyId == "" { return nil, fmt.Errorf("sdkerr: unset accessKeyId") } if secretAccessKey == "" { return nil, fmt.Errorf("sdkerr: unset secretAccessKey") } client := resty.New(). SetBaseURL("http://api.routewize.com"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("Host", "api.routewize.com"). SetHeader("User-Agent", app.AppUserAgent). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { // 生成时间 date := time.Now().UTC().Format(time.RFC1123) // 获取请求谓词 verb := req.Method // 获取访问资源 canonicalizedResource := "/" if req.URL != nil { canonicalizedResource = req.URL.Path if req.URL.RawQuery != "" { values, _ := url.ParseQuery(req.URL.RawQuery) canonicalizedResource += "?" + values.Encode() } } // 计算签名 stringToSign := verb + "\n" + date + "\n" + canonicalizedResource h := hmac.New(sha256.New, []byte(secretAccessKey)) h.Write([]byte(stringToSign)) sign := base64.StdEncoding.EncodeToString(h.Sum(nil)) // 设置请求头 req.Header.Set("Date", date) req.Header.Set("Authorization", fmt.Sprintf("QC-HMAC-SHA256 %s:%s", accessKeyId, sign)) return nil }) return &Client{client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if tcode := res.GetCode(); tcode != 0 { return resp, fmt.Errorf("sdkerr: code='%d', message='%s'", tcode, res.GetMessage()) } } } return resp, nil } ================================================ FILE: pkg/sdk3rd/qingcloud/dns/types.go ================================================ package dns type sdkResponse interface { GetCode() int GetMessage() string } type sdkResponseBase struct { Code *int `json:"code,omitempty"` Message *string `json:"message,omitempty"` } func (r *sdkResponseBase) GetCode() int { if r.Code == nil { return 0 } return *r.Code } func (r *sdkResponseBase) GetMessage() string { if r.Message == nil { return "" } return *r.Message } var _ sdkResponse = (*sdkResponseBase)(nil) type DnsRecord struct { GroupId *int64 `json:"group_id,omitempty"` GroupStatus *int32 `json:"group_status,omitempty"` Value []*DnsRecordValue `json:"value,omitempty"` Weight *int32 `json:"weight,omitempty"` } type DnsRecordValue struct { Id *int64 `json:"id,omitempty"` Type *string `json:"type,omitempty"` Value *string `json:"value,omitempty"` Line *string `json:"line,omitempty"` Ttl *int32 `json:"ttl,omitempty"` } ================================================ FILE: pkg/sdk3rd/qiniu/auth.go ================================================ package qiniu import ( "net/http" "github.com/qiniu/go-sdk/v7/auth" "github.com/qiniu/go-sdk/v7/client" ) type transport struct { http.RoundTripper mac *auth.Credentials } func newTransport(mac *auth.Credentials, tr http.RoundTripper) *transport { if tr == nil { tr = client.DefaultTransport } return &transport{tr, mac} } func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { token, err := t.mac.SignRequestV2(req) if err != nil { return nil, err } req.Header.Set("Authorization", "Qiniu "+token) return t.RoundTripper.RoundTrip(req) } ================================================ FILE: pkg/sdk3rd/qiniu/cdn.go ================================================ package qiniu import ( "context" "fmt" "net/http" "net/url" "github.com/qiniu/go-sdk/v7/auth" "github.com/qiniu/go-sdk/v7/client" ) type CdnManager struct { client *client.Client } func NewCdnManager(mac *auth.Credentials) *CdnManager { if mac == nil { mac = auth.Default() } client := &client.Client{Client: &http.Client{Transport: newTransport(mac, nil)}} return &CdnManager{client: client} } type GetDomainListResponse struct { Code *int `json:"code,omitempty"` Error *string `json:"error,omitempty"` Marker string `json:"marker"` Domains []*struct { Name string `json:"name"` Type string `json:"type"` CName string `json:"cname"` OperatingState string `json:"operatingState"` OperatingStateDesc string `json:"operatingStateDesc"` CreateAt string `json:"createAt"` ModifyAt string `json:"modifyAt"` } `json:"domains"` } func (m *CdnManager) GetDomainList(ctx context.Context, marker string, limit int) (*GetDomainListResponse, error) { query := url.Values{} if marker != "" { query.Set("marker", marker) } if limit > 0 { query.Set("limit", fmt.Sprintf("%d", limit)) } resp := new(GetDomainListResponse) if err := m.client.Call(ctx, resp, http.MethodGet, urlf("domain?%s", query.Encode()), nil); err != nil { return nil, err } return resp, nil } type GetDomainInfoResponse struct { Code *int `json:"code,omitempty"` Error *string `json:"error,omitempty"` Name string `json:"name"` Type string `json:"type"` CName string `json:"cname"` Https *struct { CertID string `json:"certId"` ForceHttps bool `json:"forceHttps"` Http2Enable bool `json:"http2Enable"` } `json:"https"` PareDomain string `json:"pareDomain"` OperationType string `json:"operationType"` OperatingState string `json:"operatingState"` OperatingStateDesc string `json:"operatingStateDesc"` CreateAt string `json:"createAt"` ModifyAt string `json:"modifyAt"` } func (m *CdnManager) GetDomainInfo(ctx context.Context, domain string) (*GetDomainInfoResponse, error) { resp := new(GetDomainInfoResponse) if err := m.client.Call(ctx, resp, http.MethodGet, urlf("domain/%s", domain), nil); err != nil { return nil, err } return resp, nil } type ModifyDomainHttpsConfRequest struct { CertID string `json:"certId"` ForceHttps bool `json:"forceHttps"` Http2Enable bool `json:"http2Enable"` } type ModifyDomainHttpsConfResponse struct { Code *int `json:"code,omitempty"` Error *string `json:"error,omitempty"` } func (m *CdnManager) ModifyDomainHttpsConf(ctx context.Context, domain string, certId string, forceHttps bool, http2Enable bool) (*ModifyDomainHttpsConfResponse, error) { req := &ModifyDomainHttpsConfRequest{ CertID: certId, ForceHttps: forceHttps, Http2Enable: http2Enable, } resp := new(ModifyDomainHttpsConfResponse) if err := m.client.CallWithJson(ctx, resp, http.MethodPut, urlf("domain/%s/httpsconf", domain), nil, req); err != nil { return nil, err } return resp, nil } type EnableDomainHttpsRequest struct { CertID string `json:"certId"` ForceHttps bool `json:"forceHttps"` Http2Enable bool `json:"http2Enable"` } type EnableDomainHttpsResponse struct { Code *int `json:"code,omitempty"` Error *string `json:"error,omitempty"` } func (m *CdnManager) EnableDomainHttps(ctx context.Context, domain string, certId string, forceHttps bool, http2Enable bool) (*EnableDomainHttpsResponse, error) { req := &EnableDomainHttpsRequest{ CertID: certId, ForceHttps: forceHttps, Http2Enable: http2Enable, } resp := new(EnableDomainHttpsResponse) if err := m.client.CallWithJson(ctx, resp, http.MethodPut, urlf("domain/%s/sslize", domain), nil, req); err != nil { return nil, err } return resp, nil } ================================================ FILE: pkg/sdk3rd/qiniu/kodo.go ================================================ package qiniu import ( "context" "net/http" "github.com/qiniu/go-sdk/v7/auth" "github.com/qiniu/go-sdk/v7/client" ) type KodoManager struct { client *client.Client } func NewKodoManager(mac *auth.Credentials) *KodoManager { if mac == nil { mac = auth.Default() } client := &client.Client{Client: &http.Client{Transport: newTransport(mac, nil)}} return &KodoManager{client: client} } type BindBucketCertRequest struct { CertID string `json:"certid"` Domain string `json:"domain"` } type BindBucketCertResponse struct { Code *int `json:"code,omitempty"` Error *string `json:"error,omitempty"` } func (m *KodoManager) BindBucketCert(ctx context.Context, domain string, certId string) (*BindBucketCertResponse, error) { req := &BindBucketCertRequest{ CertID: certId, Domain: domain, } resp := new(BindBucketCertResponse) if err := m.client.CallWithJson(ctx, resp, http.MethodPut, urlf("cert/bind"), nil, req); err != nil { return nil, err } return resp, nil } ================================================ FILE: pkg/sdk3rd/qiniu/sslcert.go ================================================ package qiniu import ( "context" "net/http" "net/url" "github.com/qiniu/go-sdk/v7/auth" "github.com/qiniu/go-sdk/v7/client" ) type SslCertManager struct { client *client.Client } func NewSslCertManager(mac *auth.Credentials) *SslCertManager { if mac == nil { mac = auth.Default() } client := &client.Client{Client: &http.Client{Transport: newTransport(mac, nil)}} return &SslCertManager{client: client} } type GetSslCertListResponse struct { Code *int `json:"code,omitempty"` Error *string `json:"error,omitempty"` Certs []*struct { CertID string `json:"certid"` Name string `json:"name"` CommonName string `json:"common_name"` DnsNames []string `json:"dnsnames"` CreateTime int64 `json:"create_time"` NotBefore int64 `json:"not_before"` NotAfter int64 `json:"not_after"` ProductType string `json:"product_type"` ProductShortName string `json:"product_short_name,omitempty"` OrderId string `json:"orderid,omitempty"` CertType string `json:"cert_type"` Encrypt string `json:"encrypt"` EncryptParameter string `json:"encryptParameter,omitempty"` Enable bool `json:"enable"` } `json:"certs"` Marker string `json:"marker"` } func (m *SslCertManager) GetSslCertList(ctx context.Context, marker string, limit int32) (*GetSslCertListResponse, error) { resp := new(GetSslCertListResponse) if err := m.client.Call(ctx, resp, http.MethodGet, urlf("sslcert?marker=%s&limit=%d", url.QueryEscape(marker), limit), nil); err != nil { return nil, err } return resp, nil } type UploadSslCertRequest struct { Name string `json:"name"` CommonName string `json:"common_name"` Certificate string `json:"ca"` PrivateKey string `json:"pri"` } type UploadSslCertResponse struct { Code *int `json:"code,omitempty"` Error *string `json:"error,omitempty"` CertID string `json:"certID"` } func (m *SslCertManager) UploadSslCert(ctx context.Context, name string, commonName string, certificate string, privateKey string) (*UploadSslCertResponse, error) { req := &UploadSslCertRequest{ Name: name, CommonName: commonName, Certificate: certificate, PrivateKey: privateKey, } resp := new(UploadSslCertResponse) if err := m.client.CallWithJson(ctx, resp, http.MethodPost, urlf("sslcert"), nil, req); err != nil { return nil, err } return resp, nil } ================================================ FILE: pkg/sdk3rd/qiniu/util.go ================================================ package qiniu import ( "fmt" "strings" ) const qiniuHost = "https://api.qiniu.com" func urlf(pathf string, pathargs ...any) string { path := fmt.Sprintf(pathf, pathargs...) path = strings.TrimPrefix(path, "/") return qiniuHost + "/" + path } ================================================ FILE: pkg/sdk3rd/rainyun/api_rcdn_instance_ssl_bind.go ================================================ package rainyun import ( "context" "fmt" "net/http" ) type RcdnInstanceSslBindRequest struct { CertId int64 `json:"cert_id"` Domains []string `json:"domains"` } type RcdnInstanceSslBindResponse struct { sdkResponseBase } func (c *Client) RcdnInstanceSslBind(instanceId int64, req *RcdnInstanceSslBindRequest) (*RcdnInstanceSslBindResponse, error) { return c.RcdnInstanceSslBindWithContext(context.Background(), instanceId, req) } func (c *Client) RcdnInstanceSslBindWithContext(ctx context.Context, instanceId int64, req *RcdnInstanceSslBindRequest) (*RcdnInstanceSslBindResponse, error) { if instanceId == 0 { return nil, fmt.Errorf("sdkerr: unset instanceId") } httpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf("/product/rcdn/instance/%d/ssl_bind", instanceId)) if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &RcdnInstanceSslBindResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/rainyun/api_ssl_center_create.go ================================================ package rainyun import ( "context" "net/http" ) type SslCenterCreateRequest struct { Cert string `json:"cert"` Key string `json:"key"` } type SslCenterCreateResponse struct { sdkResponseBase } func (c *Client) SslCenterCreate(req *SslCenterCreateRequest) (*SslCenterCreateResponse, error) { return c.SslCenterCreateWithContext(context.Background(), req) } func (c *Client) SslCenterCreateWithContext(ctx context.Context, req *SslCenterCreateRequest) (*SslCenterCreateResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/product/sslcenter/") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &SslCenterCreateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/rainyun/api_ssl_center_get.go ================================================ package rainyun import ( "context" "fmt" "net/http" ) type SslCenterGetResponse struct { sdkResponseBase Data *SslDetail `json:"data,omitempty"` } func (c *Client) SslCenterGet(sslId int64) (*SslCenterGetResponse, error) { return c.SslCenterGetWithContext(context.Background(), sslId) } func (c *Client) SslCenterGetWithContext(ctx context.Context, sslId int64) (*SslCenterGetResponse, error) { if sslId == 0 { return nil, fmt.Errorf("sdkerr: unset sslId") } httpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf("/product/sslcenter/%d", sslId)) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &SslCenterGetResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/rainyun/api_ssl_center_list.go ================================================ package rainyun import ( "context" "encoding/json" "net/http" ) type SslCenterListFilters struct { Domain *string `json:"Domain,omitempty"` } type SslCenterListRequest struct { Filters *SslCenterListFilters `json:"columnFilters,omitempty"` Sort []*string `json:"sort,omitempty"` Page *int32 `json:"page,omitempty"` PerPage *int32 `json:"perPage,omitempty"` } type SslCenterListResponse struct { sdkResponseBase Data *struct { TotalRecords int32 `json:"TotalRecords"` Records []*SslRecord `json:"Records"` } `json:"data,omitempty"` } func (c *Client) SslCenterList(req *SslCenterListRequest) (*SslCenterListResponse, error) { return c.SslCenterListWithContext(context.Background(), req) } func (c *Client) SslCenterListWithContext(ctx context.Context, req *SslCenterListRequest) (*SslCenterListResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/product/sslcenter") if err != nil { return nil, err } else { jsonb, _ := json.Marshal(req) httpreq.SetQueryParam("options", string(jsonb)) httpreq.SetContext(ctx) } result := &SslCenterListResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/rainyun/api_ssl_center_update.go ================================================ package rainyun import ( "context" "fmt" "net/http" ) type SslCenterUpdateRequest struct { Cert string `json:"cert"` Key string `json:"key"` } type SslCenterUpdateResponse struct { sdkResponseBase } func (c *Client) SslCenterUpdate(certId int64, req *SslCenterUpdateRequest) (*SslCenterUpdateResponse, error) { return c.SslCenterUpdateWithContext(context.Background(), certId, req) } func (c *Client) SslCenterUpdateWithContext(ctx context.Context, certId int64, req *SslCenterUpdateRequest) (*SslCenterUpdateResponse, error) { if certId == 0 { return nil, fmt.Errorf("sdkerr: unset certId") } httpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf("/product/sslcenter/%d", certId)) if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &SslCenterUpdateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/rainyun/client.go ================================================ package rainyun import ( "encoding/json" "fmt" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { client *resty.Client } func NewClient(apiKey string) (*Client, error) { if apiKey == "" { return nil, fmt.Errorf("sdkerr: unset apiKey") } client := resty.New(). SetBaseURL("https://api.v2.rainyun.com"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetHeader("X-API-Key", apiKey) return &Client{client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if tcode := res.GetCode(); tcode/100 != 2 { return resp, fmt.Errorf("sdkerr: code='%d', message='%s'", tcode, res.GetMessage()) } } } return resp, nil } ================================================ FILE: pkg/sdk3rd/rainyun/types.go ================================================ package rainyun type sdkResponse interface { GetCode() int GetMessage() string } type sdkResponseBase struct { Code *int `json:"code,omitempty"` Message *string `json:"message,omitempty"` } func (r *sdkResponseBase) GetCode() int { if r.Code == nil { return 0 } return *r.Code } func (r *sdkResponseBase) GetMessage() string { if r.Message == nil { return "" } return *r.Message } var _ sdkResponse = (*sdkResponseBase)(nil) type SslRecord struct { ID int64 `json:"ID"` UID int64 `json:"UID"` Domain string `json:"Domain"` Issuer string `json:"Issuer"` StartDate int64 `json:"StartDate"` ExpireDate int64 `json:"ExpDate"` UploadTime int64 `json:"UploadTime"` } type SslDetail struct { Cert string `json:"Cert"` Key string `json:"Key"` Domain string `json:"DomainName"` Issuer string `json:"Issuer"` StartDate int64 `json:"StartDate"` ExpireDate int64 `json:"ExpDate"` RemainDays int64 `json:"RemainDays"` } ================================================ FILE: pkg/sdk3rd/ratpanel/api_set_cert_update.go ================================================ package ratpanel import ( "context" "fmt" "net/http" ) type CertUpdateRequest struct { CertId int64 `json:"id"` Type string `json:"type"` Domains []string `json:"domains"` Certificate string `json:"cert"` PrivateKey string `json:"key"` } type CertUpdateResponse struct { sdkResponseBase } func (c *Client) CertUpdate(req *CertUpdateRequest) (*CertUpdateResponse, error) { return c.CertUpdateWithContext(context.Background(), req) } func (c *Client) CertUpdateWithContext(ctx context.Context, req *CertUpdateRequest) (*CertUpdateResponse, error) { httpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf("/cert/cert/%d", req.CertId)) if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &CertUpdateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ratpanel/api_set_setting_cert.go ================================================ package ratpanel import ( "context" "net/http" ) type SetSettingCertRequest struct { Certificate string `json:"cert"` PrivateKey string `json:"key"` } type SetSettingCertResponse struct { sdkResponseBase } func (c *Client) SetSettingCert(req *SetSettingCertRequest) (*SetSettingCertResponse, error) { return c.SetSettingCertWithContext(context.Background(), req) } func (c *Client) SetSettingCertWithContext(ctx context.Context, req *SetSettingCertRequest) (*SetSettingCertResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/setting/cert") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &SetSettingCertResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ratpanel/api_set_website_cert.go ================================================ package ratpanel import ( "context" "net/http" ) type SetWebsiteCertRequest struct { SiteName string `json:"name"` Certificate string `json:"cert"` PrivateKey string `json:"key"` } type SetWebsiteCertResponse struct { sdkResponseBase } func (c *Client) SetWebsiteCert(req *SetWebsiteCertRequest) (*SetWebsiteCertResponse, error) { return c.SetWebsiteCertWithContext(context.Background(), req) } func (c *Client) SetWebsiteCertWithContext(ctx context.Context, req *SetWebsiteCertRequest) (*SetWebsiteCertResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/website/cert") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &SetWebsiteCertResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/ratpanel/client.go ================================================ package ratpanel import ( "bytes" "crypto/hmac" "crypto/sha256" "crypto/tls" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { client *resty.Client } func NewClient(serverUrl string, accessTokenId int64, accessToken string) (*Client, error) { if serverUrl == "" { return nil, fmt.Errorf("sdkerr: unset serverUrl") } if _, err := url.Parse(serverUrl); err != nil { return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err) } if accessTokenId == 0 { return nil, fmt.Errorf("sdkerr: unset accessTokenId") } if accessToken == "" { return nil, fmt.Errorf("sdkerr: unset accessToken") } client := resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")+"/api"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { var body []byte var err error if req.Body != nil { body, err = io.ReadAll(req.Body) if err != nil { return err } req.Body = io.NopCloser(bytes.NewReader(body)) } canonicalPath := req.URL.Path if !strings.HasPrefix(canonicalPath, "/api") { index := strings.Index(canonicalPath, "/api") if index != -1 { canonicalPath = canonicalPath[index:] } } canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s", req.Method, canonicalPath, req.URL.Query().Encode(), sumSha256(string(body))) timestamp := time.Now().Unix() req.Header.Set("X-Timestamp", fmt.Sprintf("%d", timestamp)) stringToSign := fmt.Sprintf("%s\n%d\n%s", "HMAC-SHA256", timestamp, sumSha256(canonicalRequest)) signature := sumHmacSha256(stringToSign, accessToken) req.Header.Set("Authorization", fmt.Sprintf("HMAC-SHA256 Credential=%d, Signature=%s", accessTokenId, signature)) return nil }) return &Client{client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) SetTLSConfig(config *tls.Config) *Client { c.client.SetTLSClientConfig(config) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if tmessage := res.GetMessage(); tmessage != "success" { return resp, fmt.Errorf("sdkerr: message='%s'", tmessage) } } } return resp, nil } func sumSha256(str string) string { sum := sha256.Sum256([]byte(str)) dst := make([]byte, hex.EncodedLen(len(sum))) hex.Encode(dst, sum[:]) return string(dst) } func sumHmacSha256(data string, secret string) string { h := hmac.New(sha256.New, []byte(secret)) h.Write([]byte(data)) return hex.EncodeToString(h.Sum(nil)) } ================================================ FILE: pkg/sdk3rd/ratpanel/types.go ================================================ package ratpanel type sdkResponse interface { GetMessage() string } type sdkResponseBase struct { Message *string `json:"msg,omitempty"` } func (r *sdkResponseBase) GetMessage() string { if r.Message == nil { return "" } return *r.Message } var _ sdkResponse = (*sdkResponseBase)(nil) ================================================ FILE: pkg/sdk3rd/safeline/api_update_certificate.go ================================================ package safeline import ( "context" "net/http" ) type UpdateCertificateRequest struct { Id int64 `json:"id"` Type int32 `json:"type"` Manual *CertificateManul `json:"manual"` } type UpdateCertificateResponse struct { sdkResponseBase } func (c *Client) UpdateCertificate(req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) { return c.UpdateCertificateWithContext(context.Background(), req) } func (c *Client) UpdateCertificateWithContext(ctx context.Context, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/api/open/cert") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &UpdateCertificateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/safeline/client.go ================================================ package safeline import ( "crypto/tls" "encoding/json" "fmt" "net/url" "strings" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { client *resty.Client } func NewClient(serverUrl, apiToken string) (*Client, error) { if serverUrl == "" { return nil, fmt.Errorf("sdkerr: unset serverUrl") } if _, err := url.Parse(serverUrl); err != nil { return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err) } if apiToken == "" { return nil, fmt.Errorf("sdkerr: unset apiToken") } client := resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetHeader("X-SLCE-API-TOKEN", apiToken) return &Client{client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) SetTLSConfig(config *tls.Config) *Client { c.client.SetTLSClientConfig(config) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if terrcode := res.GetErrCode(); terrcode != "" { return resp, fmt.Errorf("sdkerr: err='%s', msg='%s'", terrcode, res.GetErrMsg()) } } } return resp, nil } ================================================ FILE: pkg/sdk3rd/safeline/types.go ================================================ package safeline type sdkResponse interface { GetErrCode() string GetErrMsg() string } type sdkResponseBase struct { ErrCode *string `json:"err,omitempty"` ErrMsg *string `json:"msg,omitempty"` } func (r *sdkResponseBase) GetErrCode() string { if r.ErrCode == nil { return "" } return *r.ErrCode } func (r *sdkResponseBase) GetErrMsg() string { if r.ErrMsg == nil { return "" } return *r.ErrMsg } var _ sdkResponse = (*sdkResponseBase)(nil) type CertificateManul struct { Crt string `json:"crt"` Key string `json:"key"` } ================================================ FILE: pkg/sdk3rd/synologydsm/api_auth_login.go ================================================ package synologydsm import ( "fmt" "net/http" "net/url" "strconv" qs "github.com/google/go-querystring/query" ) type LoginRequest struct { Account string `json:"account" url:"account"` Password string `json:"passwd" url:"passwd"` OtpCode string `json:"otp_code,omitempty" url:"otp_code,omitempty"` } type LoginResponse struct { sdkResponseBase Data *struct { Sid string `json:"sid"` SynoToken string `json:"synotoken"` DeviceId string `json:"device_id,omitempty"` Did string `json:"did,omitempty"` } `json:"data,omitempty"` } func (c *Client) Login(req *LoginRequest) (*LoginResponse, error) { const AUTH_API_NAME = "SYNO.API.Auth" if c.authApiPath == "" || c.authApiVersion == 0 { queryInfoReq := &QueryAPIInfoRequest{ Query: AUTH_API_NAME, } queryInfoResp, err := c.QueryAPIInfo(queryInfoReq) if err != nil { return nil, fmt.Errorf("sdkerr: failed to query API info: %w", err) } else { authApiInfo, ok := queryInfoResp.Data[AUTH_API_NAME] if !ok { return nil, fmt.Errorf("sdkerr: failed to query API info: \"%s\" not found", AUTH_API_NAME) } c.authApiPath = authApiInfo.Path c.authApiVersion = authApiInfo.MaxVersion } } params := url.Values{ "api": {AUTH_API_NAME}, "version": {strconv.Itoa(c.authApiVersion)}, "method": {"login"}, "format": {"sid"}, "enable_syno_token": {"yes"}, "enable_device_token": {"yes"}, "device_name": {"Certimate"}, } values, err := qs.Values(req) if err != nil { return nil, err } for k := range values { params.Set(k, values.Get(k)) } httpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf("/webapi/%s?%s", c.authApiPath, params.Encode())) if err != nil { return nil, err } result := &LoginResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { if result != nil && result.GetErrorCode() > 0 { errcode := result.GetErrorCode() errdesc := getAuthErrorDescription(errcode) return result, fmt.Errorf("sdkerr: code='%d', desc='%s'", errcode, errdesc) } return result, err } if result.Data.Sid == "" || result.Data.SynoToken == "" { return result, fmt.Errorf("sdkerr: login succeeded but the sid or synotoken is empty") } c.synoTokenMtx.Lock() defer c.synoTokenMtx.Unlock() c.sid = result.Data.Sid c.synoToken = result.Data.SynoToken return result, nil } ================================================ FILE: pkg/sdk3rd/synologydsm/api_auth_logout.go ================================================ package synologydsm import ( "fmt" "net/http" "net/url" "strconv" ) type LogoutResponse struct { sdkResponseBase } func (c *Client) Logout() (*LogoutResponse, error) { if c.sid == "" { result := &LogoutResponse{} result.Success = true return result, nil } params := url.Values{ "api": {"SYNO.API.Auth"}, "version": {strconv.Itoa(c.authApiVersion)}, "method": {"logout"}, "_sid": {c.sid}, } httpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf("/webapi/%s?%s", c.authApiPath, params.Encode())) if err != nil { return nil, err } result := &LogoutResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } c.synoTokenMtx.Lock() defer c.synoTokenMtx.Unlock() c.sid = "" c.synoToken = "" return result, nil } ================================================ FILE: pkg/sdk3rd/synologydsm/api_core_certificate_crt_list.go ================================================ package synologydsm import ( "net/http" "net/url" ) type ListCertificatesResponse struct { sdkResponseBase Data *struct { Certificates []*CertificateInfo `json:"certificates"` } `json:"data,omitempty"` } func (c *Client) ListCertificates() (*ListCertificatesResponse, error) { params := url.Values{ "api": {"SYNO.Core.Certificate.CRT"}, "method": {"list"}, "version": {"1"}, "_sid": {c.sid}, } httpreq, err := c.newRequest(http.MethodPost, "/webapi/entry.cgi") if err != nil { return nil, err } else { httpreq.SetHeader("Content-Type", "application/x-www-form-urlencoded") httpreq.SetFormDataFromValues(params) } result := &ListCertificatesResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/synologydsm/api_core_certificate_import.go ================================================ package synologydsm import ( "fmt" "net/http" "net/url" "strings" ) type ImportCertificateRequest struct { ID string `json:"id" url:"id"` Description string `json:"desc" url:"desc"` Key string `json:"key" url:"key"` Cert string `json:"cert" url:"cert"` InterCert string `json:"inter_cert" url:"inter_cert"` AsDefault bool `json:"as_default" url:"as_default"` } type ImportCertificateResponse struct { sdkResponseBase Data *struct { RestartHttpd bool `json:"restart_httpd"` } `json:"data,omitempty"` } func (c *Client) ImportCertificate(req *ImportCertificateRequest) (*ImportCertificateResponse, error) { params := url.Values{ "api": {"SYNO.Core.Certificate"}, "method": {"import"}, "version": {"1"}, "_sid": {c.sid}, "SynoToken": {c.synoToken}, } httpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf("/webapi/entry.cgi?%s", params.Encode())) if err != nil { return nil, err } else { httpreq.SetMultipartField("key", "key.pem", "text/plain", strings.NewReader(req.Key)) httpreq.SetMultipartField("cert", "cert.pem", "text/plain", strings.NewReader(req.Cert)) httpreq.SetMultipartField("inter_cert", "chain.pem", "text/plain", strings.NewReader(req.InterCert)) httpreq.SetMultipartField("id", "", "", strings.NewReader(req.ID)) httpreq.SetMultipartField("desc", "", "", strings.NewReader(req.Description)) if req.AsDefault { httpreq.SetMultipartField("as_default", "", "", strings.NewReader("true")) } } result := &ImportCertificateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/synologydsm/api_core_certificate_service_set.go ================================================ package synologydsm import ( "encoding/json" "fmt" "net/http" "net/url" ) type ServiceCertificateSetting struct { Service *CertificateService `json:"service"` OldCertID string `json:"old_id"` CertID string `json:"id"` } type SetServiceCertificateRequest struct { Settings []*ServiceCertificateSetting `json:"settings"` } type SetServiceCertificateResponse struct { sdkResponseBase } func (c *Client) SetServiceCertificate(req *SetServiceCertificateRequest) (*SetServiceCertificateResponse, error) { bsettings, _ := json.Marshal(req.Settings) params := url.Values{ "api": {"SYNO.Core.Certificate.Service"}, "method": {"set"}, "version": {"1"}, "settings": {string(bsettings)}, } httpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf("/webapi/entry.cgi?_sid=%s", c.sid)) if err != nil { return nil, err } else { httpreq.SetHeader("Content-Type", "application/x-www-form-urlencoded") httpreq.SetFormDataFromValues(params) } result := &SetServiceCertificateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/synologydsm/api_info_query.go ================================================ package synologydsm import ( "fmt" "net/http" "net/url" qs "github.com/google/go-querystring/query" ) type QueryAPIInfoRequest struct { Query string `json:"query" url:"query"` } type QueryAPIInfoResponse struct { sdkResponseBase Data map[string]APIInfo `json:"data,omitempty"` } func (c *Client) QueryAPIInfo(req *QueryAPIInfoRequest) (*QueryAPIInfoResponse, error) { params := url.Values{ "api": {"SYNO.API.Info"}, "version": {"1"}, "method": {"query"}, } values, err := qs.Values(req) if err != nil { return nil, err } for k := range values { params.Set(k, values.Get(k)) } httpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf("/webapi/query.cgi?%s", params.Encode())) if err != nil { return nil, err } result := &QueryAPIInfoResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/synologydsm/client.go ================================================ package synologydsm import ( "crypto/tls" "encoding/json" "fmt" "net/http" "net/url" "strings" "sync" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { authApiPath string authApiVersion int sid string synoToken string synoTokenMtx sync.Mutex client *resty.Client } func NewClient(serverUrl string) (*Client, error) { if serverUrl == "" { return nil, fmt.Errorf("sdkerr: unset serverUrl") } if _, err := url.Parse(serverUrl); err != nil { return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err) } client := &Client{} client.client = resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")). SetHeader("User-Agent", app.AppUserAgent). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { if client.synoToken != "" { req.Header.Set("X-SYNO-TOKEN", client.synoToken) } return nil }) return client, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) SetTLSConfig(config *tls.Config) *Client { c.client.SetTLSClientConfig(config) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if tsuccess := res.GetSuccess(); !tsuccess { return resp, fmt.Errorf("sdkerr: code='%d'", res.GetErrorCode()) } } return resp, nil } ================================================ FILE: pkg/sdk3rd/synologydsm/types.go ================================================ package synologydsm type sdkResponse interface { GetSuccess() bool GetErrorCode() int } type sdkResponseBase struct { Success bool `json:"success"` Error *APIError `json:"error,omitempty"` } func (r *sdkResponseBase) GetSuccess() bool { return r.Success } func (r *sdkResponseBase) GetErrorCode() int { if r.Error == nil { if r.Success { return 0 } return -1 } return r.Error.Code } var _ sdkResponse = (*sdkResponseBase)(nil) type APIError struct { Code int `json:"code,omitempty"` } type APIInfo struct { Path string `json:"path"` MinVersion int `json:"minVersion"` MaxVersion int `json:"maxVersion"` } type CertificateInfo struct { ID string `json:"id"` Description string `json:"desc"` IsDefault bool `json:"is_default"` IsBroken bool `json:"is_broken"` Issuer struct { CommonName string `json:"common_name"` Country string `json:"country"` Organization string `json:"organization"` } `json:"issuer"` Subject struct { CommonName string `json:"common_name"` Country string `json:"country"` Organization string `json:"organization"` SAN []string `json:"sub_alt_name"` } `json:"subject"` ValidFrom string `json:"valid_from"` ValidTill string `json:"valid_till"` SignatureAlgorithm string `json:"signature_algorithm"` Renewable bool `json:"renewable"` Services []*CertificateService `json:"services"` } type CertificateService struct { DisplayName string `json:"display_name"` DisplayNameI18N string `json:"display_name_i18n,omitempty"` IsPkg bool `json:"isPkg"` Owner string `json:"owner"` Service string `json:"service"` Subscriber string `json:"subscriber"` } ================================================ FILE: pkg/sdk3rd/synologydsm/utils.go ================================================ package synologydsm func getAuthErrorDescription(code int) string { switch code { case 100: return "Unknown error" case 101: return "Invalid parameters" case 102: return "API does not exist" case 103: return "Method does not exist" case 104: return "This API version is not supported" case 105: return "Insufficient user privilege" case 106: return "Connection time out" case 107: return "Multiple login detected" case 400: return "Invalid password or account does not exist" case 401: return "Guest or disabled account" case 402: return "Permission denied" case 403: return "2-factor authentication code required (OTP)" case 404: return "Failed to authenticate 2-factor authentication code" case 405: return "Server version is too low or not supported" case 406: return "2-factor authentication code expired" case 407: return "Login failed: IP has been blocked" case 408: return "Expired password" case 409: return "Password must be changed (password policy)" case 410: return "Account locked (too many failed login attempts)" default: return "Unknown authentication error" } } ================================================ FILE: pkg/sdk3rd/ucloud/ucdn/api_get_ucdn_domain_config.go ================================================ package ucdn import ( ucloudcdn "github.com/ucloud/ucloud-sdk-go/services/ucdn" ) type GetUcdnDomainConfigRequest = ucloudcdn.GetUcdnDomainConfigRequest type GetUcdnDomainConfigResponse = ucloudcdn.GetUcdnDomainConfigResponse func (c *UCDNClient) NewGetUcdnDomainConfigRequest() *GetUcdnDomainConfigRequest { req := &GetUcdnDomainConfigRequest{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *UCDNClient) GetUcdnDomainConfig(req *GetUcdnDomainConfigRequest) (*GetUcdnDomainConfigResponse, error) { var err error var res GetUcdnDomainConfigResponse reqCopier := *req err = c.Client.InvokeAction("GetUcdnDomainConfig", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/ucdn/api_update_ucdn_domain_https_config_v2.go ================================================ package ucdn import ( ucloudcdn "github.com/ucloud/ucloud-sdk-go/services/ucdn" ) type UpdateUcdnDomainHttpsConfigV2Request = ucloudcdn.UpdateUcdnDomainHttpsConfigV2Request type UpdateUcdnDomainHttpsConfigV2Response = ucloudcdn.UpdateUcdnDomainHttpsConfigV2Response func (c *UCDNClient) NewUpdateUcdnDomainHttpsConfigV2Request() *UpdateUcdnDomainHttpsConfigV2Request { req := &UpdateUcdnDomainHttpsConfigV2Request{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *UCDNClient) UpdateUcdnDomainHttpsConfigV2(req *UpdateUcdnDomainHttpsConfigV2Request) (*UpdateUcdnDomainHttpsConfigV2Response, error) { var err error var res UpdateUcdnDomainHttpsConfigV2Response reqCopier := *req err = c.Client.InvokeAction("UpdateUcdnDomainHttpsConfigV2", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/ucdn/client.go ================================================ package ucdn import ( "github.com/ucloud/ucloud-sdk-go/ucloud" "github.com/ucloud/ucloud-sdk-go/ucloud/auth" ) type UCDNClient struct { *ucloud.Client } func NewClient(config *ucloud.Config, credential *auth.Credential) *UCDNClient { meta := ucloud.ClientMeta{Product: "UCDN"} client := ucloud.NewClientWithMeta(config, credential, meta) return &UCDNClient{ client, } } ================================================ FILE: pkg/sdk3rd/ucloud/ucdn/types.go ================================================ package ucdn import ( ucloudcdn "github.com/ucloud/ucloud-sdk-go/services/ucdn" ) type DomainConfigInfo = ucloudcdn.DomainConfigInfo ================================================ FILE: pkg/sdk3rd/ucloud/udnr/api_add_domain_dns.go ================================================ package udnr import ( "github.com/ucloud/ucloud-sdk-go/ucloud/request" "github.com/ucloud/ucloud-sdk-go/ucloud/response" ) type AddDomainDNSRequest struct { request.CommonBase Dn *string `required:"true"` DnsType *string `required:"true"` RecordName *string `required:"true"` Content *string `required:"true"` TTL *string `required:"true"` Prio *string `required:"false"` } type AddDomainDNSResponse struct { response.CommonBase } func (c *UDNRClient) NewAddDomainDNSRequest() *AddDomainDNSRequest { req := &AddDomainDNSRequest{} c.Client.SetupRequest(req) req.SetRetryable(false) return req } func (c *UDNRClient) AddDomainDNS(req *AddDomainDNSRequest) (*AddDomainDNSResponse, error) { var err error var res AddDomainDNSResponse reqCopier := *req err = c.Client.InvokeAction("UdnrDomainDNSAdd", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/udnr/api_delete_domain_dns.go ================================================ package udnr import ( "github.com/ucloud/ucloud-sdk-go/ucloud/request" "github.com/ucloud/ucloud-sdk-go/ucloud/response" ) type DeleteDomainDNSRequest struct { request.CommonBase Dn *string `required:"true"` DnsType *string `required:"true"` RecordName *string `required:"true"` Content *string `required:"true"` } type DeleteDomainDNSResponse struct { response.CommonBase } func (c *UDNRClient) NewDeleteDomainDNSRequest() *DeleteDomainDNSRequest { req := &DeleteDomainDNSRequest{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *UDNRClient) DeleteDomainDNS(req *DeleteDomainDNSRequest) (*DeleteDomainDNSResponse, error) { var err error var res DeleteDomainDNSResponse reqCopier := *req err = c.Client.InvokeAction("UdnrDeleteDnsRecord", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/udnr/api_query_domain_dns.go ================================================ package udnr import ( "github.com/ucloud/ucloud-sdk-go/ucloud/request" "github.com/ucloud/ucloud-sdk-go/ucloud/response" ) type QueryDomainDNSRequest struct { request.CommonBase Dn *string `required:"true"` } type QueryDomainDNSResponse struct { response.CommonBase Data []DomainDNSRecord } func (c *UDNRClient) NewQueryDomainDNSRequest() *QueryDomainDNSRequest { req := &QueryDomainDNSRequest{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *UDNRClient) QueryDomainDNS(req *QueryDomainDNSRequest) (*QueryDomainDNSResponse, error) { var err error var res QueryDomainDNSResponse reqCopier := *req err = c.Client.InvokeAction("UdnrDomainDNSQuery", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/udnr/client.go ================================================ package udnr import ( "github.com/ucloud/ucloud-sdk-go/ucloud" "github.com/ucloud/ucloud-sdk-go/ucloud/auth" ) type UDNRClient struct { *ucloud.Client } func NewClient(config *ucloud.Config, credential *auth.Credential) *UDNRClient { meta := ucloud.ClientMeta{Product: "UDNR"} client := ucloud.NewClientWithMeta(config, credential, meta) return &UDNRClient{ client, } } ================================================ FILE: pkg/sdk3rd/ucloud/udnr/types.go ================================================ package udnr type DomainDNSRecord struct { DnsType string RecordName string Content string TTL string Prio string } ================================================ FILE: pkg/sdk3rd/ucloud/uewaf/api_add_waf_domain_certificate_info.go ================================================ package uewaf import ( "github.com/ucloud/ucloud-sdk-go/ucloud/request" "github.com/ucloud/ucloud-sdk-go/ucloud/response" ) type AddWafDomainCertificateInfoRequest struct { request.CommonBase Domain *string `required:"true"` CertificateName *string `required:"true"` SslPublicKey *string `required:"true"` SslPrivateKey *string `required:"false"` SslMD *string `required:"false"` SslKeyLess *string `required:"false"` } type AddWafDomainCertificateInfoResponse struct { response.CommonBase Id int } func (c *UEWAFClient) NewAddWafDomainCertificateInfoRequest() *AddWafDomainCertificateInfoRequest { req := &AddWafDomainCertificateInfoRequest{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *UEWAFClient) AddWafDomainCertificateInfo(req *AddWafDomainCertificateInfoRequest) (*AddWafDomainCertificateInfoResponse, error) { var err error var res AddWafDomainCertificateInfoResponse reqCopier := *req err = c.Client.InvokeAction("AddWafDomainCertificateInfo", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/uewaf/client.go ================================================ package uewaf import ( "github.com/ucloud/ucloud-sdk-go/ucloud" "github.com/ucloud/ucloud-sdk-go/ucloud/auth" ) type UEWAFClient struct { *ucloud.Client } func NewClient(config *ucloud.Config, credential *auth.Credential) *UEWAFClient { meta := ucloud.ClientMeta{Product: "UEWAF"} client := ucloud.NewClientWithMeta(config, credential, meta) return &UEWAFClient{ client, } } ================================================ FILE: pkg/sdk3rd/ucloud/ufile/api_add_ufile_ssl_cert.go ================================================ package ufile import ( "github.com/ucloud/ucloud-sdk-go/ucloud/request" "github.com/ucloud/ucloud-sdk-go/ucloud/response" ) type AddUFileSSLCertRequest struct { request.CommonBase BucketName *string `required:"true"` Domain *string `required:"true"` CertificateName *string `required:"true"` USSLId *string `required:"false"` } type AddUFileSSLCertResponse struct { response.CommonBase } func (c *UFileClient) NewAddUFileSSLCertRequest() *AddUFileSSLCertRequest { req := &AddUFileSSLCertRequest{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *UFileClient) AddUFileSSLCert(req *AddUFileSSLCertRequest) (*AddUFileSSLCertResponse, error) { var err error var res AddUFileSSLCertResponse reqCopier := *req err = c.Client.InvokeAction("AddUFileSSLCert", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/ufile/client.go ================================================ package ufile import ( "github.com/ucloud/ucloud-sdk-go/ucloud" "github.com/ucloud/ucloud-sdk-go/ucloud/auth" ) type UFileClient struct { *ucloud.Client } func NewClient(config *ucloud.Config, credential *auth.Credential) *UFileClient { meta := ucloud.ClientMeta{Product: "UFile"} client := ucloud.NewClientWithMeta(config, credential, meta) return &UFileClient{ client, } } ================================================ FILE: pkg/sdk3rd/ucloud/ulb/api_add_ssl_binding.go ================================================ package ulb import ( ucloudlb "github.com/ucloud/ucloud-sdk-go/services/ulb" ) type AddSSLBindingRequest = ucloudlb.AddSSLBindingRequest type AddSSLBindingResponse = ucloudlb.AddSSLBindingResponse func (c *ULBClient) NewAddSSLBindingRequest() *AddSSLBindingRequest { req := &AddSSLBindingRequest{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *ULBClient) AddSSLBinding(req *AddSSLBindingRequest) (*AddSSLBindingResponse, error) { var err error var res AddSSLBindingResponse reqCopier := *req err = c.Client.InvokeAction("AddSSLBinding", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/ulb/api_bind_ssl.go ================================================ package ulb import ( ucloudlb "github.com/ucloud/ucloud-sdk-go/services/ulb" ) type BindSSLRequest = ucloudlb.BindSSLRequest type BindSSLResponse = ucloudlb.BindSSLResponse func (c *ULBClient) NewBindSSLRequest() *BindSSLRequest { req := &BindSSLRequest{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *ULBClient) BindSSL(req *BindSSLRequest) (*BindSSLResponse, error) { var err error var res BindSSLResponse reqCopier := *req err = c.Client.InvokeAction("BindSSL", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/ulb/api_create_ssl.go ================================================ package ulb import ( ucloudlb "github.com/ucloud/ucloud-sdk-go/services/ulb" ) type CreateSSLRequest = ucloudlb.CreateSSLRequest type CreateSSLResponse = ucloudlb.CreateSSLResponse func (c *ULBClient) NewCreateSSLRequest() *CreateSSLRequest { req := &CreateSSLRequest{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *ULBClient) CreateSSL(req *CreateSSLRequest) (*CreateSSLResponse, error) { var err error var res CreateSSLResponse reqCopier := *req err = c.Client.InvokeAction("CreateSSL", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/ulb/api_delete_ssl_binding.go ================================================ package ulb import ( ucloudlb "github.com/ucloud/ucloud-sdk-go/services/ulb" ) type DeleteSSLBindingRequest = ucloudlb.DeleteSSLBindingRequest type DeleteSSLBindingResponse = ucloudlb.DeleteSSLBindingResponse func (c *ULBClient) NewDeleteSSLBindingRequest() *DeleteSSLBindingRequest { req := &DeleteSSLBindingRequest{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *ULBClient) DeleteSSLBinding(req *DeleteSSLBindingRequest) (*DeleteSSLBindingResponse, error) { var err error var res DeleteSSLBindingResponse reqCopier := *req err = c.Client.InvokeAction("DeleteSSLBinding", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/ulb/api_describe_listeners.go ================================================ package ulb import ( ucloudlb "github.com/ucloud/ucloud-sdk-go/services/ulb" ) type DescribeListenersRequest = ucloudlb.DescribeListenersRequest type DescribeListenersResponse = ucloudlb.DescribeListenersResponse func (c *ULBClient) NewDescribeListenersRequest() *DescribeListenersRequest { req := &DescribeListenersRequest{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *ULBClient) DescribeListeners(req *DescribeListenersRequest) (*DescribeListenersResponse, error) { var err error var res DescribeListenersResponse reqCopier := *req err = c.Client.InvokeAction("DescribeListeners", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/ulb/api_describe_ssl.go ================================================ package ulb import ( ucloudlb "github.com/ucloud/ucloud-sdk-go/services/ulb" ) type DescribeSSLRequest = ucloudlb.DescribeSSLRequest type DescribeSSLResponse = ucloudlb.DescribeSSLResponse func (c *ULBClient) NewDescribeSSLRequest() *DescribeSSLRequest { req := &DescribeSSLRequest{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *ULBClient) DescribeSSL(req *DescribeSSLRequest) (*DescribeSSLResponse, error) { var err error var res DescribeSSLResponse reqCopier := *req err = c.Client.InvokeAction("DescribeSSL", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/ulb/api_describe_ssl_v2.go ================================================ package ulb import ( ucloudlb "github.com/ucloud/ucloud-sdk-go/services/ulb" ) type DescribeSSLV2Request = ucloudlb.DescribeSSLV2Request type DescribeSSLV2Response = ucloudlb.DescribeSSLV2Response func (c *ULBClient) NewDescribeSSLV2Request() *DescribeSSLV2Request { req := &DescribeSSLV2Request{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *ULBClient) DescribeSSLV2(req *DescribeSSLV2Request) (*DescribeSSLV2Response, error) { var err error var res DescribeSSLV2Response reqCopier := *req err = c.Client.InvokeAction("DescribeSSLV2", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/ulb/api_describe_vserver.go ================================================ package ulb import ( ucloudlb "github.com/ucloud/ucloud-sdk-go/services/ulb" ) type DescribeVServerRequest = ucloudlb.DescribeVServerRequest type DescribeVServerResponse = ucloudlb.DescribeVServerResponse func (c *ULBClient) NewDescribeVServerRequest() *DescribeVServerRequest { req := &DescribeVServerRequest{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *ULBClient) DescribeVServer(req *DescribeVServerRequest) (*DescribeVServerResponse, error) { var err error var res DescribeVServerResponse reqCopier := *req err = c.Client.InvokeAction("DescribeVServer", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/ulb/api_unbind_ssl.go ================================================ package ulb import ( ucloudlb "github.com/ucloud/ucloud-sdk-go/services/ulb" ) type UnbindSSLRequest = ucloudlb.UnbindSSLRequest type UnbindSSLResponse = ucloudlb.UnbindSSLResponse func (c *ULBClient) NewUnbindSSLRequest() *UnbindSSLRequest { req := &UnbindSSLRequest{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *ULBClient) UnbindSSL(req *UnbindSSLRequest) (*UnbindSSLResponse, error) { var err error var res UnbindSSLResponse reqCopier := *req err = c.Client.InvokeAction("UnbindSSL", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/ulb/api_update_listener_attribute.go ================================================ package ulb import ( ucloudlb "github.com/ucloud/ucloud-sdk-go/services/ulb" ) type UpdateListenerAttributeRequest = ucloudlb.UpdateListenerAttributeRequest type UpdateListenerAttributeResponse = ucloudlb.UpdateListenerAttributeResponse func (c *ULBClient) NewUpdateListenerAttributeRequest() *UpdateListenerAttributeRequest { req := &UpdateListenerAttributeRequest{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *ULBClient) UpdateListenerAttribute(req *UpdateListenerAttributeRequest) (*UpdateListenerAttributeResponse, error) { var err error var res UpdateListenerAttributeResponse reqCopier := *req err = c.Client.InvokeAction("UpdateListenerAttribute", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/ulb/client.go ================================================ package ulb import ( "github.com/ucloud/ucloud-sdk-go/ucloud" "github.com/ucloud/ucloud-sdk-go/ucloud/auth" ) type ULBClient struct { *ucloud.Client } func NewClient(config *ucloud.Config, credential *auth.Credential) *ULBClient { meta := ucloud.ClientMeta{Product: "ULB"} client := ucloud.NewClientWithMeta(config, credential, meta) return &ULBClient{ client, } } ================================================ FILE: pkg/sdk3rd/ucloud/upathx/api_bind_pathx_ssl.go ================================================ package upathx import ( ucloudpathx "github.com/ucloud/ucloud-sdk-go/services/pathx" ) type BindPathXSSLRequest = ucloudpathx.BindPathXSSLRequest type BindPathXSSLResponse = ucloudpathx.BindPathXSSLResponse func (c *UPathXClient) NewBindPathXSSLRequest() *BindPathXSSLRequest { req := &BindPathXSSLRequest{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *UPathXClient) BindPathXSSL(req *BindPathXSSLRequest) (*BindPathXSSLResponse, error) { var err error var res BindPathXSSLResponse reqCopier := *req err = c.Client.InvokeAction("BindPathXSSL", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/upathx/api_create_pathx_ssl.go ================================================ package upathx import ( ucloudpathx "github.com/ucloud/ucloud-sdk-go/services/pathx" ) type CreatePathXSSLRequest = ucloudpathx.CreatePathXSSLRequest type CreatePathXSSLResponse = ucloudpathx.CreatePathXSSLResponse func (c *UPathXClient) NewCreatePathXSSLRequest() *CreatePathXSSLRequest { req := &CreatePathXSSLRequest{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *UPathXClient) CreatePathXSSL(req *CreatePathXSSLRequest) (*CreatePathXSSLResponse, error) { var err error var res CreatePathXSSLResponse reqCopier := *req err = c.Client.InvokeAction("CreatePathXSSL", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/upathx/api_describe_pathx_ssl.go ================================================ package upathx import ( ucloudpathx "github.com/ucloud/ucloud-sdk-go/services/pathx" ) type DescribePathXSSLRequest = ucloudpathx.DescribePathXSSLRequest type DescribePathXSSLResponse = ucloudpathx.DescribePathXSSLResponse func (c *UPathXClient) NewDescribePathXSSLRequest() *DescribePathXSSLRequest { req := &DescribePathXSSLRequest{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *UPathXClient) DescribePathXSSL(req *DescribePathXSSLRequest) (*DescribePathXSSLResponse, error) { var err error var res DescribePathXSSLResponse reqCopier := *req err = c.Client.InvokeAction("DescribePathXSSL", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/upathx/api_unbind_pathx_ssl.go ================================================ package upathx import ( ucloudpathx "github.com/ucloud/ucloud-sdk-go/services/pathx" ) type UnbindPathXSSLRequest = ucloudpathx.UnBindPathXSSLRequest type UnbindPathXSSLResponse = ucloudpathx.UnBindPathXSSLResponse func (c *UPathXClient) NewUnbindPathXSSLRequest() *UnbindPathXSSLRequest { req := &UnbindPathXSSLRequest{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *UPathXClient) UnbindPathXSSL(req *UnbindPathXSSLRequest) (*UnbindPathXSSLResponse, error) { var err error var res UnbindPathXSSLResponse reqCopier := *req err = c.Client.InvokeAction("UnBindPathXSSL", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/upathx/client.go ================================================ package upathx import ( "github.com/ucloud/ucloud-sdk-go/ucloud" "github.com/ucloud/ucloud-sdk-go/ucloud/auth" ) type UPathXClient struct { *ucloud.Client } func NewClient(config *ucloud.Config, credential *auth.Credential) *UPathXClient { meta := ucloud.ClientMeta{Product: "PathX"} client := ucloud.NewClientWithMeta(config, credential, meta) return &UPathXClient{ client, } } ================================================ FILE: pkg/sdk3rd/ucloud/ussl/api_download_certificate.go ================================================ package ussl import ( "github.com/ucloud/ucloud-sdk-go/ucloud/request" "github.com/ucloud/ucloud-sdk-go/ucloud/response" ) type DownloadCertificateRequest struct { request.CommonBase CertificateID *int `required:"true"` } type DownloadCertificateResponse struct { response.CommonBase CertificateUrl string CertCA *CertificateDownloadInfo Certificate *CertificateDownloadInfo } func (c *USSLClient) NewDownloadCertificateRequest() *DownloadCertificateRequest { req := &DownloadCertificateRequest{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *USSLClient) DownloadCertificate(req *DownloadCertificateRequest) (*DownloadCertificateResponse, error) { var err error var res DownloadCertificateResponse reqCopier := *req err = c.Client.InvokeAction("DownloadCertificate", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/ussl/api_get_certificate_detail_info.go ================================================ package ussl import ( "github.com/ucloud/ucloud-sdk-go/ucloud/request" "github.com/ucloud/ucloud-sdk-go/ucloud/response" ) type GetCertificateDetailInfoRequest struct { request.CommonBase CertificateID *int `required:"true"` } type GetCertificateDetailInfoResponse struct { response.CommonBase CertificateInfo *CertificateInfo } func (c *USSLClient) NewGetCertificateDetailInfoRequest() *GetCertificateDetailInfoRequest { req := &GetCertificateDetailInfoRequest{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *USSLClient) GetCertificateDetailInfo(req *GetCertificateDetailInfoRequest) (*GetCertificateDetailInfoResponse, error) { var err error var res GetCertificateDetailInfoResponse reqCopier := *req err = c.Client.InvokeAction("GetCertificateDetailInfo", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/ussl/api_get_certificate_list.go ================================================ package ussl import ( "github.com/ucloud/ucloud-sdk-go/ucloud/request" "github.com/ucloud/ucloud-sdk-go/ucloud/response" ) type GetCertificateListRequest struct { request.CommonBase Mode *string `required:"true"` StateCode *string `required:"false"` Brand *string `required:"false"` CaOrganization *string `required:"false"` Domain *string `required:"false"` Sort *string `required:"false"` Page *int `required:"false"` PageSize *int `required:"false"` } type GetCertificateListResponse struct { response.CommonBase CertificateList []*CertificateListItem TotalCount int } func (c *USSLClient) NewGetCertificateListRequest() *GetCertificateListRequest { req := &GetCertificateListRequest{} c.Client.SetupRequest(req) req.SetRetryable(true) return req } func (c *USSLClient) GetCertificateList(req *GetCertificateListRequest) (*GetCertificateListResponse, error) { var err error var res GetCertificateListResponse reqCopier := *req err = c.Client.InvokeAction("GetCertificateList", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/ussl/api_upload_normal_certificate.go ================================================ package ussl import ( "github.com/ucloud/ucloud-sdk-go/ucloud/request" "github.com/ucloud/ucloud-sdk-go/ucloud/response" ) type UploadNormalCertificateRequest struct { request.CommonBase CertificateName *string `required:"true"` SslPublicKey *string `required:"true"` SslPrivateKey *string `required:"true"` SslMD5 *string `required:"true"` SslCaKey *string `required:"false"` } type UploadNormalCertificateResponse struct { response.CommonBase CertificateID int LongResourceID string } func (c *USSLClient) NewUploadNormalCertificateRequest() *UploadNormalCertificateRequest { req := &UploadNormalCertificateRequest{} c.Client.SetupRequest(req) req.SetRetryable(false) return req } func (c *USSLClient) UploadNormalCertificate(req *UploadNormalCertificateRequest) (*UploadNormalCertificateResponse, error) { var err error var res UploadNormalCertificateResponse reqCopier := *req err = c.Client.InvokeAction("UploadNormalCertificate", &reqCopier, &res) if err != nil { return &res, err } return &res, nil } ================================================ FILE: pkg/sdk3rd/ucloud/ussl/client.go ================================================ package ussl import ( "github.com/ucloud/ucloud-sdk-go/ucloud" "github.com/ucloud/ucloud-sdk-go/ucloud/auth" ) type USSLClient struct { *ucloud.Client } func NewClient(config *ucloud.Config, credential *auth.Credential) *USSLClient { meta := ucloud.ClientMeta{Product: "USSL"} client := ucloud.NewClientWithMeta(config, credential, meta) return &USSLClient{ client, } } ================================================ FILE: pkg/sdk3rd/ucloud/ussl/types.go ================================================ package ussl type CertificateListItem struct { CertificateID int CertificateSN string CertificateCat string Mode string Domains string Brand string ValidityPeriod int Type string NotBefore int NotAfter int AlarmState int State string StateCode string Name string MaxDomainsCount int DomainsCount int CaChannel string CSRAlgorithms []CSRAlgorithmInfo TopOrganizationID int OrganizationID int IsFree int YearOfValidity int Channel int CreateTime int CertificateUrl string } type CSRAlgorithmInfo struct { Algorithm string AlgorithmOption []string } type CertificateInfo struct { Type string CertificateID int CertificateType string CaOrganization string Algorithm string ValidityPeriod int State string StateCode string Name string Brand string Domains string DomainsCount int Mode string CSROnline int CSR string CSRKeyParameter string CSREncryptAlgo string IssuedDate int ExpiredDate int } type CertificateDownloadInfo struct { FileData string FileName string } ================================================ FILE: pkg/sdk3rd/upyun/console/api_get_buckets.go ================================================ package console import ( "context" "net/http" qs "github.com/google/go-querystring/query" ) type GetBucketsRequest struct { BucketName string `json:"status" url:"bucket_name"` BusinessType string `json:"business_type" url:"business_type"` Type string `json:"type" url:"type"` Status string `json:"state" url:"state"` Tag string `json:"tag" url:"tag"` IsSecurityCDN bool `json:"security_cdn" url:"security_cdn"` WithDomains bool `json:"with_domains" url:"with_domains"` Page int32 `json:"page" url:"page"` PerPage int32 `json:"perPage" url:"perPage"` } type GetBucketsResponse struct { sdkResponseBase Data *struct { sdkResponseBaseData Buckets []*BucketInfo `json:"buckets"` Pager BucketPager `json:"pager"` } `json:"data,omitempty"` } type BucketInfo struct { BucketName string `json:"bucket_name"` BusinessType string `json:"business_type"` Type string `json:"type"` Status string `json:"status"` Tag string `json:"tag"` IsFusionCDN bool `json:"fusion_cdn"` IsSecurityCDN bool `json:"security_cdn"` Domains []*BucketDomain `json:"domains"` Visible bool `json:"visible"` CreatedAt string `json:"created_at"` } type BucketDomain struct { Domain string `json:"domain"` Status string `json:"status"` } type BucketPager struct { Page int32 `json:"page"` Pages int64 `json:"pages"` Total int64 `json:"total"` } func (c *Client) GetBuckets(req *GetBucketsRequest) (*GetBucketsResponse, error) { return c.GetBucketsWithContext(context.Background(), req) } func (c *Client) GetBucketsWithContext(ctx context.Context, req *GetBucketsRequest) (*GetBucketsResponse, error) { if err := c.ensureCookieExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodGet, "/api/v2/buckets") if err != nil { return nil, err } else { values, err := qs.Values(req) if err != nil { return nil, err } httpreq.SetQueryParamsFromValues(values) httpreq.SetContext(ctx) } result := &GetBucketsResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/upyun/console/api_get_https_certificate_manager.go ================================================ package console import ( "context" "fmt" "net/http" ) type HttpsCertificateManagerDomain struct { Name string `json:"name"` Type string `json:"type"` BucketId int64 `json:"bucket_id"` BucketName string `json:"bucket_name"` } type GetHttpsCertificateManagerResponse struct { sdkResponseBase Data *struct { sdkResponseBaseData AuthenticateNum int32 `json:"authenticate_num"` AuthenticateDomains []string `json:"authenticate_domain"` Domains []HttpsCertificateManagerDomain `json:"domains"` } `json:"data,omitempty"` } func (c *Client) GetHttpsCertificateManager(certificateId string) (*GetHttpsCertificateManagerResponse, error) { return c.GetHttpsCertificateManagerWithContext(context.Background(), certificateId) } func (c *Client) GetHttpsCertificateManagerWithContext(ctx context.Context, certificateId string) (*GetHttpsCertificateManagerResponse, error) { if certificateId == "" { return nil, fmt.Errorf("sdkerr: unset certificateId") } if err := c.ensureCookieExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodGet, "/api/https/certificate/manager/") if err != nil { return nil, err } else { httpreq.SetQueryParam("certificate_id", certificateId) httpreq.SetContext(ctx) } result := &GetHttpsCertificateManagerResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/upyun/console/api_get_https_service_manager.go ================================================ package console import ( "context" "fmt" "net/http" ) type GetHttpsServiceManagerResponse struct { sdkResponseBase Data *struct { sdkResponseBaseData Status int32 `json:"status"` Domains []HttpsServiceManagerDomain `json:"result"` } `json:"data,omitempty"` } type HttpsServiceManagerDomain struct { CertificateId string `json:"certificate_id"` CommonName string `json:"commonName"` Https bool `json:"https"` ForceHttps bool `json:"force_https"` PaymentType string `json:"payment_type"` DomainType string `json:"domain_type"` Validity HttpsServiceManagerDomainValidity `json:"validity"` } type HttpsServiceManagerDomainValidity struct { Start int64 `json:"start"` End int64 `json:"end"` } func (c *Client) GetHttpsServiceManager(domain string) (*GetHttpsServiceManagerResponse, error) { return c.GetHttpsServiceManagerWithContext(context.Background(), domain) } func (c *Client) GetHttpsServiceManagerWithContext(ctx context.Context, domain string) (*GetHttpsServiceManagerResponse, error) { if domain == "" { return nil, fmt.Errorf("sdkerr: unset domain") } if err := c.ensureCookieExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodGet, "/api/https/services/manager") if err != nil { return nil, err } else { httpreq.SetQueryParam("domain", domain) httpreq.SetContext(ctx) } result := &GetHttpsServiceManagerResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/upyun/console/api_migrate_https_domain.go ================================================ package console import ( "context" "net/http" ) type MigrateHttpsDomainRequest struct { CertificateId string `json:"crt_id"` Domain string `json:"domain_name"` } type MigrateHttpsDomainResponse struct { sdkResponseBase Data *struct { sdkResponseBaseData Status bool `json:"status"` } `json:"data,omitempty"` } func (c *Client) MigrateHttpsDomain(req *MigrateHttpsDomainRequest) (*MigrateHttpsDomainResponse, error) { return c.MigrateHttpsDomainWithContext(context.Background(), req) } func (c *Client) MigrateHttpsDomainWithContext(ctx context.Context, req *MigrateHttpsDomainRequest) (*MigrateHttpsDomainResponse, error) { if err := c.ensureCookieExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodPost, "/api/https/migrate/domain") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &MigrateHttpsDomainResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/upyun/console/api_update_https_certificate_manager.go ================================================ package console import ( "context" "net/http" ) type UpdateHttpsCertificateManagerRequest struct { CertificateId string `json:"certificate_id"` Domain string `json:"domain"` Https bool `json:"https"` ForceHttps bool `json:"force_https"` } type UpdateHttpsCertificateManagerResponse struct { sdkResponseBase Data *struct { sdkResponseBaseData Status bool `json:"status"` } `json:"data,omitempty"` } func (c *Client) UpdateHttpsCertificateManager(req *UpdateHttpsCertificateManagerRequest) (*UpdateHttpsCertificateManagerResponse, error) { return c.UpdateHttpsCertificateManagerWithContext(context.Background(), req) } func (c *Client) UpdateHttpsCertificateManagerWithContext(ctx context.Context, req *UpdateHttpsCertificateManagerRequest) (*UpdateHttpsCertificateManagerResponse, error) { if err := c.ensureCookieExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodPost, "/api/https/certificate/manager") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &UpdateHttpsCertificateManagerResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/upyun/console/api_upload_https_certificate.go ================================================ package console import ( "context" "net/http" ) type UploadHttpsCertificateRequest struct { Certificate string `json:"certificate"` PrivateKey string `json:"private_key"` } type UploadHttpsCertificateResponse struct { sdkResponseBase Data *struct { sdkResponseBaseData Status int32 `json:"status"` Result struct { CertificateId string `json:"certificate_id"` CommonName string `json:"commonName"` Serial string `json:"serial"` } `json:"result"` } `json:"data,omitempty"` } func (c *Client) UploadHttpsCertificate(req *UploadHttpsCertificateRequest) (*UploadHttpsCertificateResponse, error) { return c.UploadHttpsCertificateWithContext(context.Background(), req) } func (c *Client) UploadHttpsCertificateWithContext(ctx context.Context, req *UploadHttpsCertificateRequest) (*UploadHttpsCertificateResponse, error) { if err := c.ensureCookieExists(); err != nil { return nil, err } httpreq, err := c.newRequest(http.MethodPost, "/api/https/certificate/") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &UploadHttpsCertificateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/upyun/console/client.go ================================================ package console import ( "encoding/json" "errors" "fmt" "net/http" "sync" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { username string password string loginCookie string loginCookieMtx sync.Mutex client *resty.Client } func NewClient(username, password string) (*Client, error) { if username == "" { return nil, fmt.Errorf("sdkerr: unset username") } if password == "" { return nil, fmt.Errorf("sdkerr: unset password") } client := &Client{ username: username, password: password, } client.client = resty.New(). SetBaseURL("https://console.upyun.com"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { if client.loginCookie != "" { req.Header.Set("Cookie", client.loginCookie) } return nil }) return client, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { tresp := &sdkResponseBase{} if err := json.Unmarshal(resp.Body(), &tresp); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else if tdata := tresp.GetData(); tdata == nil { return resp, fmt.Errorf("sdkerr: received empty data") } else if terrcode := tdata.GetErrorCode(); terrcode != 0 { return resp, fmt.Errorf("sdkerr: code='%d', message='%s'", terrcode, tdata.GetMessage()) } } } return resp, nil } func (c *Client) ensureCookieExists() error { c.loginCookieMtx.Lock() defer c.loginCookieMtx.Unlock() if c.loginCookie != "" { return nil } httpreq, err := c.newRequest(http.MethodPost, "/accounts/signin/") if err != nil { return err } else { httpreq.SetBody(map[string]string{ "username": c.username, "password": c.password, }) } type signinResponse struct { sdkResponseBase Data *struct { sdkResponseBaseData Result bool `json:"result"` } `json:"data,omitempty"` } result := &signinResponse{} httpresp, err := c.doRequestWithResult(httpreq, result) if err != nil { return err } else if !result.Data.Result { return errors.New("sdkerr: failed to signin upyun console") } else { c.loginCookie = httpresp.Header().Get("Set-Cookie") } return nil } ================================================ FILE: pkg/sdk3rd/upyun/console/types.go ================================================ package console import ( "encoding/json" ) type sdkResponse interface { GetData() *sdkResponseBaseData } type sdkResponseBase struct { Data *sdkResponseBaseData `json:"data,omitempty"` } func (r *sdkResponseBase) GetData() *sdkResponseBaseData { return r.Data } var _ sdkResponse = (*sdkResponseBase)(nil) type sdkResponseBaseData struct { ErrorCode json.Number `json:"error_code,omitempty"` Message string `json:"message,omitempty"` } func (r *sdkResponseBaseData) GetErrorCode() int { if r.ErrorCode.String() == "" { return 0 } errcode, err := r.ErrorCode.Int64() if err != nil { return -1 } return int(errcode) } func (r *sdkResponseBaseData) GetMessage() string { return r.Message } ================================================ FILE: pkg/sdk3rd/wangsu/cdn/api_batch_update_certificate_config.go ================================================ package cdn import ( "context" "net/http" ) type BatchUpdateCertificateConfigRequest struct { CertificateId int64 `json:"certificateId"` DomainNames []string `json:"domainNames"` } type BatchUpdateCertificateConfigResponse struct { sdkResponseBase } func (c *Client) BatchUpdateCertificateConfig(req *BatchUpdateCertificateConfigRequest) (*BatchUpdateCertificateConfigResponse, error) { return c.BatchUpdateCertificateConfigWithContext(context.Background(), req) } func (c *Client) BatchUpdateCertificateConfigWithContext(ctx context.Context, req *BatchUpdateCertificateConfigRequest) (*BatchUpdateCertificateConfigResponse, error) { httpreq, err := c.newRequest(http.MethodPut, "/api/config/certificate/batch") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &BatchUpdateCertificateConfigResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/wangsu/cdn/client.go ================================================ package cdn import ( "fmt" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/pkg/sdk3rd/wangsu/openapi" ) type Client struct { client *openapi.Client } func NewClient(accessKey, secretKey string) (*Client, error) { client, err := openapi.NewClient(accessKey, secretKey) if err != nil { return nil, err } return &Client{client: client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { return c.client.NewRequest(method, path) } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { return c.client.DoRequest(req) } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { resp, err := c.client.DoRequestWithResult(req, res) if err == nil { if tcode := res.GetCode(); tcode != "" && tcode != "0" { return resp, fmt.Errorf("sdkerr: api error: code='%s', message='%s'", tcode, res.GetMessage()) } } return resp, err } ================================================ FILE: pkg/sdk3rd/wangsu/cdn/types.go ================================================ package cdn type sdkResponse interface { GetCode() string GetMessage() string } type sdkResponseBase struct { Code *string `json:"code,omitempty"` Message *string `json:"message,omitempty"` } var _ sdkResponse = (*sdkResponseBase)(nil) func (r *sdkResponseBase) GetCode() string { if r.Code == nil { return "" } return *r.Code } func (r *sdkResponseBase) GetMessage() string { if r.Message == nil { return "" } return *r.Message } ================================================ FILE: pkg/sdk3rd/wangsu/cdnpro/api_create_certificate.go ================================================ package cdnpro import ( "context" "fmt" "net/http" ) type CreateCertificateRequest struct { Timestamp int64 `json:"-"` Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` AutoRenew *string `json:"autoRenew,omitempty"` ForceRenew *bool `json:"forceRenew,omitempty"` NewVersion *CertificateVersionInfo `json:"newVersion,omitempty"` } type CreateCertificateResponse struct { sdkResponseBase CertificateLocation string `json:"location,omitempty"` } func (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) { return c.CreateCertificateWithContext(context.Background(), req) } func (c *Client) CreateCertificateWithContext(ctx context.Context, req *CreateCertificateRequest) (*CreateCertificateResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/cdn/certificates") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetHeader("X-CNC-Timestamp", fmt.Sprintf("%d", req.Timestamp)) httpreq.SetContext(ctx) } result := &CreateCertificateResponse{} if httpresp, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } else { result.CertificateLocation = httpresp.Header().Get("Location") } return result, nil } ================================================ FILE: pkg/sdk3rd/wangsu/cdnpro/api_create_deployment_task.go ================================================ package cdnpro import ( "context" "net/http" ) type CreateDeploymentTaskRequest struct { Name *string `json:"name,omitempty"` Target *string `json:"target,omitempty"` Actions *[]DeploymentTaskActionInfo `json:"actions,omitempty"` Webhook *string `json:"webhook,omitempty"` } type CreateDeploymentTaskResponse struct { sdkResponseBase DeploymentTaskLocation string `json:"location,omitempty"` } func (c *Client) CreateDeploymentTask(req *CreateDeploymentTaskRequest) (*CreateDeploymentTaskResponse, error) { return c.CreateDeploymentTaskWithContext(context.Background(), req) } func (c *Client) CreateDeploymentTaskWithContext(ctx context.Context, req *CreateDeploymentTaskRequest) (*CreateDeploymentTaskResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/cdn/deploymentTasks") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &CreateDeploymentTaskResponse{} if httpresp, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } else { result.DeploymentTaskLocation = httpresp.Header().Get("Location") } return result, nil } ================================================ FILE: pkg/sdk3rd/wangsu/cdnpro/api_get_deployment_task_detail.go ================================================ package cdnpro import ( "context" "fmt" "net/http" "net/url" ) type GetDeploymentTaskDetailResponse struct { sdkResponseBase Name string `json:"name"` Target string `json:"target"` Actions []DeploymentTaskActionInfo `json:"actions"` Status string `json:"status"` StatusDetails string `json:"statusDetails"` SubmissionTime string `json:"submissionTime"` FinishTime string `json:"finishTime"` ApiRequestId string `json:"apiRequestId"` } func (c *Client) GetDeploymentTaskDetail(deploymentTaskId string) (*GetDeploymentTaskDetailResponse, error) { return c.GetDeploymentTaskDetailWithContext(context.Background(), deploymentTaskId) } func (c *Client) GetDeploymentTaskDetailWithContext(ctx context.Context, deploymentTaskId string) (*GetDeploymentTaskDetailResponse, error) { if deploymentTaskId == "" { return nil, fmt.Errorf("sdkerr: unset deploymentTaskId") } httpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf("/cdn/deploymentTasks/%s", url.PathEscape(deploymentTaskId))) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &GetDeploymentTaskDetailResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/wangsu/cdnpro/api_get_hostname_detail.go ================================================ package cdnpro import ( "context" "fmt" "net/http" "net/url" ) type GetHostnameDetailResponse struct { sdkResponseBase Hostname string `json:"hostname"` PropertyInProduction *HostnamePropertyInfo `json:"propertyInProduction,omitempty"` PropertyInStaging *HostnamePropertyInfo `json:"propertyInStaging,omitempty"` } func (c *Client) GetHostnameDetail(hostname string) (*GetHostnameDetailResponse, error) { return c.GetHostnameDetailWithContext(context.Background(), hostname) } func (c *Client) GetHostnameDetailWithContext(ctx context.Context, hostname string) (*GetHostnameDetailResponse, error) { if hostname == "" { return nil, fmt.Errorf("sdkerr: unset hostname") } httpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf("/cdn/hostnames/%s", url.PathEscape(hostname))) if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &GetHostnameDetailResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/wangsu/cdnpro/api_update_certificate.go ================================================ package cdnpro import ( "context" "fmt" "net/http" "net/url" ) type UpdateCertificateRequest struct { Timestamp int64 `json:"-"` Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` AutoRenew *string `json:"autoRenew,omitempty"` ForceRenew *bool `json:"forceRenew,omitempty"` NewVersion *CertificateVersionInfo `json:"newVersion,omitempty"` } type UpdateCertificateResponse struct { sdkResponseBase CertificateLocation string `json:"location,omitempty"` } func (c *Client) UpdateCertificate(certificateId string, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) { return c.UpdateCertificateWithContext(context.Background(), certificateId, req) } func (c *Client) UpdateCertificateWithContext(ctx context.Context, certificateId string, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) { if certificateId == "" { return nil, fmt.Errorf("sdkerr: unset certificateId") } httpreq, err := c.newRequest(http.MethodPatch, fmt.Sprintf("/cdn/certificates/%s", url.PathEscape(certificateId))) if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetHeader("X-CNC-Timestamp", fmt.Sprintf("%d", req.Timestamp)) httpreq.SetContext(ctx) } result := &UpdateCertificateResponse{} if httpresp, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } else { result.CertificateLocation = httpresp.Header().Get("Location") } return result, nil } ================================================ FILE: pkg/sdk3rd/wangsu/cdnpro/client.go ================================================ package cdnpro import ( "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/pkg/sdk3rd/wangsu/openapi" ) type Client struct { client *openapi.Client } func NewClient(accessKey, secretKey string) (*Client, error) { client, err := openapi.NewClient(accessKey, secretKey) if err != nil { return nil, err } return &Client{client: client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { return c.client.NewRequest(method, path) } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { return c.client.DoRequest(req) } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { return c.client.DoRequestWithResult(req, res) } ================================================ FILE: pkg/sdk3rd/wangsu/cdnpro/types.go ================================================ package cdnpro type sdkResponse interface { GetCode() string GetMessage() string } type sdkResponseBase struct { Code *string `json:"code,omitempty"` Message *string `json:"message,omitempty"` } var _ sdkResponse = (*sdkResponseBase)(nil) func (r *sdkResponseBase) GetCode() string { if r.Code == nil { return "" } return *r.Code } func (r *sdkResponseBase) GetMessage() string { if r.Message == nil { return "" } return *r.Message } type CertificateVersionInfo struct { Comments *string `json:"comments,omitempty"` PrivateKey *string `json:"privateKey,omitempty"` Certificate *string `json:"certificate,omitempty"` ChainCert *string `json:"chainCert,omitempty"` IdentificationInfo *CertificateVersionIdentificationInfo `json:"identificationInfo,omitempty"` } type CertificateVersionIdentificationInfo struct { Country *string `json:"country,omitempty"` State *string `json:"state,omitempty"` City *string `json:"city,omitempty"` Company *string `json:"company,omitempty"` Department *string `json:"department,omitempty"` CommonName *string `json:"commonName,omitempty"` Email *string `json:"email,omitempty"` SubjectAlternativeNames *[]string `json:"subjectAlternativeNames,omitempty"` } type HostnamePropertyInfo struct { PropertyId string `json:"propertyId"` Version int32 `json:"version"` CertificateId *string `json:"certificateId,omitempty"` } type DeploymentTaskActionInfo struct { Action *string `json:"action,omitempty"` PropertyId *string `json:"propertyId,omitempty"` CertificateId *string `json:"certificateId,omitempty"` Version *int32 `json:"version,omitempty"` } ================================================ FILE: pkg/sdk3rd/wangsu/certificate/api_create_certificate.go ================================================ package certificate import ( "context" "net/http" ) type CreateCertificateRequest struct { Name *string `json:"name,omitempty"` Certificate *string `json:"certificate,omitempty"` PrivateKey *string `json:"privateKey,omitempty"` Comment *string `json:"comment,omitempty" ` } type CreateCertificateResponse struct { sdkResponseBase CertificateLocation string `json:"location,omitempty"` } func (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) { return c.CreateCertificateWithContext(context.Background(), req) } func (c *Client) CreateCertificateWithContext(ctx context.Context, req *CreateCertificateRequest) (*CreateCertificateResponse, error) { httpreq, err := c.newRequest(http.MethodPost, "/api/certificate") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &CreateCertificateResponse{} if httpresp, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } else { result.CertificateLocation = httpresp.Header().Get("Location") } return result, nil } ================================================ FILE: pkg/sdk3rd/wangsu/certificate/api_list_certificates.go ================================================ package certificate import ( "context" "net/http" ) type ListCertificatesResponse struct { sdkResponseBase Certificates []*CertificateRecord `json:"ssl-certificates,omitempty"` } func (c *Client) ListCertificates() (*ListCertificatesResponse, error) { return c.ListCertificatesWithContext(context.Background()) } func (c *Client) ListCertificatesWithContext(ctx context.Context) (*ListCertificatesResponse, error) { httpreq, err := c.newRequest(http.MethodGet, "/api/ssl/certificate") if err != nil { return nil, err } else { httpreq.SetContext(ctx) } result := &ListCertificatesResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/wangsu/certificate/api_update_certificate.go ================================================ package certificate import ( "context" "fmt" "net/http" "net/url" ) type UpdateCertificateRequest struct { Name *string `json:"name,omitempty"` Certificate *string `json:"certificate,omitempty"` PrivateKey *string `json:"privateKey,omitempty"` Comment *string `json:"comment,omitempty" ` } type UpdateCertificateResponse struct { sdkResponseBase } func (c *Client) UpdateCertificate(certificateId string, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) { return c.UpdateCertificateWithContext(context.Background(), certificateId, req) } func (c *Client) UpdateCertificateWithContext(ctx context.Context, certificateId string, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) { if certificateId == "" { return nil, fmt.Errorf("sdkerr: unset certificateId") } httpreq, err := c.newRequest(http.MethodPut, fmt.Sprintf("/api/certificate/%s", url.PathEscape(certificateId))) if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &UpdateCertificateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/wangsu/certificate/client.go ================================================ package certificate import ( "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/pkg/sdk3rd/wangsu/openapi" ) type Client struct { client *openapi.Client } func NewClient(accessKey, secretKey string) (*Client, error) { client, err := openapi.NewClient(accessKey, secretKey) if err != nil { return nil, err } return &Client{client: client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(method string, path string) (*resty.Request, error) { return c.client.NewRequest(method, path) } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { return c.client.DoRequest(req) } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { return c.client.DoRequestWithResult(req, res) } ================================================ FILE: pkg/sdk3rd/wangsu/certificate/types.go ================================================ package certificate type sdkResponse interface { GetCode() string GetMessage() string } type sdkResponseBase struct { Code *string `json:"code,omitempty"` Message *string `json:"message,omitempty"` } var _ sdkResponse = (*sdkResponseBase)(nil) func (r *sdkResponseBase) GetCode() string { if r.Code == nil { return "" } return *r.Code } func (r *sdkResponseBase) GetMessage() string { if r.Message == nil { return "" } return *r.Message } type CertificateRecord struct { CertificateId string `json:"certificate-id"` Name string `json:"name"` Comment string `json:"comment"` ValidityFrom string `json:"certificate-validity-from"` ValidityTo string `json:"certificate-validity-to"` Serial string `json:"certificate-serial"` } ================================================ FILE: pkg/sdk3rd/wangsu/openapi/client.go ================================================ package openapi import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "strings" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { accessKey string secretKey string client *resty.Client } func NewClient(accessKey, secretKey string) (*Client, error) { if accessKey == "" { return nil, fmt.Errorf("sdkerr: unset accessKey") } if secretKey == "" { return nil, fmt.Errorf("sdkerr: unset secretKey") } client := resty.New(). SetBaseURL("https://open.chinanetcenter.com"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("Host", "open.chinanetcenter.com"). SetHeader("User-Agent", app.AppUserAgent). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { // Step 1: Get request method method := req.Method method = strings.ToUpper(method) // Step 2: Get request path path := "/" if req.URL != nil { path = req.URL.Path } // Step 3: Get unencoded query string queryString := "" if method != http.MethodPost && req.URL != nil { queryString = req.URL.RawQuery s, err := url.QueryUnescape(queryString) if err != nil { return err } queryString = s } // Step 4: Get canonical headers & signed headers canonicalHeaders := "" + "content-type:" + strings.TrimSpace(strings.ToLower(req.Header.Get("Content-Type"))) + "\n" + "host:" + strings.TrimSpace(strings.ToLower(req.Header.Get("Host"))) + "\n" signedHeaders := "content-type;host" // Step 5: Get request payload payload := "" if method != http.MethodGet && req.Body != nil { reader, err := req.GetBody() if err != nil { return err } defer reader.Close() payloadb, err := io.ReadAll(reader) if err != nil { return err } payload = string(payloadb) } hashedPayload := sha256.Sum256([]byte(payload)) hashedPayloadHex := strings.ToLower(hex.EncodeToString(hashedPayload[:])) // Step 6: Get timestamp var reqtime time.Time timestampString := req.Header.Get("X-CNC-Timestamp") if timestampString == "" { reqtime = time.Now().UTC() timestampString = fmt.Sprintf("%d", reqtime.Unix()) } else { timestamp, err := strconv.ParseInt(timestampString, 10, 64) if err != nil { return err } reqtime = time.Unix(timestamp, 0).UTC() } // Step 7: Get canonical request string canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", method, path, queryString, canonicalHeaders, signedHeaders, hashedPayloadHex) hashedCanonicalRequest := sha256.Sum256([]byte(canonicalRequest)) hashedCanonicalRequestHex := strings.ToLower(hex.EncodeToString(hashedCanonicalRequest[:])) // Step 8: String to sign const SignAlgorithmHeader = "CNC-HMAC-SHA256" stringToSign := fmt.Sprintf("%s\n%s\n%s", SignAlgorithmHeader, timestampString, hashedCanonicalRequestHex) hmac := hmac.New(sha256.New, []byte(secretKey)) hmac.Write([]byte(stringToSign)) sign := hmac.Sum(nil) signHex := strings.ToLower(hex.EncodeToString(sign)) // Step 9: Add headers to request req.Header.Set("X-CNC-AccessKey", accessKey) req.Header.Set("X-CNC-Timestamp", timestampString) req.Header.Set("X-CNC-Auth-Method", "AKSK") req.Header.Set("Authorization", fmt.Sprintf("%s Credential=%s, SignedHeaders=%s, Signature=%s", SignAlgorithmHeader, accessKey, signedHeaders, signHex)) req.Header.Set("Date", reqtime.Format("Mon, 02 Jan 2006 15:04:05 GMT")) return nil }) return &Client{ accessKey: accessKey, secretKey: secretKey, client: client, }, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) NewRequest(method string, path string) (*resty.Request, error) { if method == "" { return nil, fmt.Errorf("sdkerr: unset method") } if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = method req.URL = path return req, nil } func (c *Client) DoRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) DoRequestWithResult(req *resty.Request, res interface{}) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.DoRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } } return resp, nil } ================================================ FILE: pkg/sdk3rd/xinnet/api_dns_create.go ================================================ package xinnet import ( "context" ) type DnsCreateRequest struct { DomainName *string `json:"domainName,omitempty"` RecordName *string `json:"recordName,omitempty"` Type *string `json:"type,omitempty"` Value *string `json:"value,omitempty"` Line *string `json:"line,omitempty"` Ttl *int32 `json:"ttl,omitempty"` Mx *int32 `json:"mx,omitempty"` Status *int32 `json:"status,omitempty"` } type DnsCreateResponse struct { sdkResponseBase Data *int64 `json:"data,omitempty"` } func (c *Client) DnsCreate(req *DnsCreateRequest) (*DnsCreateResponse, error) { return c.DnsCreateWithContext(context.Background(), req) } func (c *Client) DnsCreateWithContext(ctx context.Context, req *DnsCreateRequest) (*DnsCreateResponse, error) { httpreq, err := c.newRequest("/dns/create/") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &DnsCreateResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/xinnet/api_dns_delete.go ================================================ package xinnet import ( "context" ) type DnsDeleteRequest struct { DomainName *string `json:"domainName,omitempty"` RecordId *int64 `json:"recordId,omitempty"` } type DnsDeleteResponse struct { sdkResponseBase } func (c *Client) DnsDelete(req *DnsDeleteRequest) (*DnsDeleteResponse, error) { return c.DnsDeleteWithContext(context.Background(), req) } func (c *Client) DnsDeleteWithContext(ctx context.Context, req *DnsDeleteRequest) (*DnsDeleteResponse, error) { httpreq, err := c.newRequest("/dns/delete/") if err != nil { return nil, err } else { httpreq.SetBody(req) httpreq.SetContext(ctx) } result := &DnsDeleteResponse{} if _, err := c.doRequestWithResult(httpreq, result); err != nil { return result, err } return result, nil } ================================================ FILE: pkg/sdk3rd/xinnet/client.go ================================================ package xinnet import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "strings" "time" "github.com/go-resty/resty/v2" "github.com/certimate-go/certimate/internal/app" ) type Client struct { client *resty.Client } func NewClient(agentId, appSecret string) (*Client, error) { if agentId == "" { return nil, fmt.Errorf("sdkerr: unset agentId") } if appSecret == "" { return nil, fmt.Errorf("sdkerr: unset appSecret") } client := resty.New(). SetBaseURL("https://apiv2.xinnet.com/api"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { // 生成时间戳 timestamp := time.Now().UTC().Format("20060102T150405Z") // 获取请求路径,注意结尾必须是 "/" urlPath := "/" if req.URL != nil { urlPath = req.URL.Path if !strings.HasSuffix(urlPath, "/") { urlPath += "/" } } // 获取请求方法 requestMethod := req.Method // 获取请求体 requestBody := "" if req.Body != nil { reader, err := req.GetBody() if err != nil { return err } defer reader.Close() payloadb, err := io.ReadAll(reader) if err != nil { return err } requestBody = string(payloadb) } // 计算签名 algorithm := "HMAC-SHA256" stringToSign := algorithm + "\n" + timestamp + "\n" + requestMethod + "\n" + urlPath + "\n" + requestBody h := hmac.New(sha256.New, []byte(appSecret)) h.Write([]byte(stringToSign)) signature := hex.EncodeToString(h.Sum(nil)) // 设置请求头 req.Header.Set("timestamp", timestamp) req.Header.Set("authorization", fmt.Sprintf("%s Access=%s, Signature=%s", algorithm, agentId, signature)) return nil }) return &Client{client}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { c.client.SetTimeout(timeout) return c } func (c *Client) newRequest(path string) (*resty.Request, error) { if path == "" { return nil, fmt.Errorf("sdkerr: unset path") } req := c.client.R() req.Method = http.MethodPost req.URL = path return req, nil } func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } // WARN: // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD. resp, err := req.Send() if err != nil { return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) } else if resp.IsError() { return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) } return resp, nil } func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) { if req == nil { return nil, fmt.Errorf("sdkerr: nil request") } resp, err := c.doRequest(req) if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &res) } return resp, err } if len(resp.Body()) != 0 { if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) } else { if tcode := res.GetCode(); tcode != "0" { return resp, fmt.Errorf("sdkerr: code='%s', msg='%s'", tcode, res.GetMessage()) } } } return resp, nil } ================================================ FILE: pkg/sdk3rd/xinnet/types.go ================================================ package xinnet type sdkResponse interface { GetCode() string GetMessage() string } type sdkResponseBase struct { Code *string `json:"code,omitempty"` Message *string `json:"message,omitempty"` RequestId *string `json:"requestId,omitempty"` } func (r *sdkResponseBase) GetCode() string { if r.Code == nil { return "" } return *r.Code } func (r *sdkResponseBase) GetMessage() string { if r.Message == nil { return "" } return *r.Message } var _ sdkResponse = (*sdkResponseBase)(nil) ================================================ FILE: pkg/utils/cert/common.go ================================================ package cert import ( "encoding/pem" ) func decodePEMBlocks(data []byte) []*pem.Block { blocks := make([]*pem.Block, 0) for { block, rest := pem.Decode(data) if block == nil { break } blocks = append(blocks, block) data = rest } return blocks } ================================================ FILE: pkg/utils/cert/comparer.go ================================================ package cert import ( "crypto/x509" ) // 比较两个 x509.Certificate 对象,判断它们是否是同一张证书。 // // 入参: // - a: 待比较的第一个 x509.Certificate 对象。 // - b: 待比较的第二个 x509.Certificate 对象。 // // 出参: // - 是否相同。 func EqualCertificates(a, b *x509.Certificate) bool { if a == nil || b == nil { return false } return a.Equal(b) } // 与 [EqualCertificates] 方法类似,但入参是 PEM 编码的证书字符串。 // // 入参: // - a: 待比较的第一个证书 PEM 内容。 // - b: 待比较的第二个证书 PEM 内容。 // // 出参: // - 是否相同。 func EqualCertificatesFromPEM(a, b string) bool { aCert, _ := ParseCertificateFromPEM(a) bCert, _ := ParseCertificateFromPEM(b) return EqualCertificates(aCert, bCert) } ================================================ FILE: pkg/utils/cert/converter.go ================================================ package cert import ( "crypto/ecdsa" "crypto/x509" "encoding/pem" "errors" "fmt" ) // 将 x509.Certificate 对象转换为 PEM 编码的字符串。 // // 入参: // - cert: x509.Certificate 对象。 // // 出参: // - certPEM: 证书 PEM 内容。 // - err: 错误。 func ConvertCertificateToPEM(cert *x509.Certificate) (_certPEM string, _err error) { if cert == nil { return "", errors.New("the input certificate is nil") } block := &pem.Block{ Type: "CERTIFICATE", Bytes: cert.Raw, } return string(pem.EncodeToMemory(block)), nil } // 将 ecdsa.PrivateKey 对象转换为 PEM 编码的字符串。 // // 入参: // - privkey: ecdsa.PrivateKey 对象。 // // 出参: // - privkeyPEM: 私钥 PEM 内容。 // - err: 错误。 func ConvertECPrivateKeyToPEM(privkey *ecdsa.PrivateKey) (_privkeyPEM string, _err error) { if privkey == nil { return "", errors.New("the input private key is nil") } data, _err := x509.MarshalECPrivateKey(privkey) if _err != nil { return "", fmt.Errorf("failed to marshal EC private key: %w", _err) } block := &pem.Block{ Type: "EC PRIVATE KEY", Bytes: data, } return string(pem.EncodeToMemory(block)), nil } ================================================ FILE: pkg/utils/cert/extractor.go ================================================ package cert import ( "encoding/pem" "errors" "fmt" ) // 从 PEM 编码的证书字符串解析并提取服务器证书和中间证书。 // // 入参: // - certPEM: 证书 PEM 内容。 // // 出参: // - serverCertPEM: 服务器证书的 PEM 内容。 // - intermediaCertPEM: 中间证书的 PEM 内容。 // - err: 错误。 func ExtractCertificatesFromPEM(certPEM string) (_serverCertPEM string, _intermediaCertPEM string, _err error) { blocks := decodePEMBlocks([]byte(certPEM)) for i, block := range blocks { if block.Type != "CERTIFICATE" { return "", "", fmt.Errorf("invalid PEM block type at %d, expected 'CERTIFICATE', got '%s'", i, block.Type) } } serverCertPEM := "" intermediaCertPEM := "" if len(blocks) == 0 { return "", "", errors.New("failed to decode PEM block") } if len(blocks) > 0 { serverCertPEM = string(pem.EncodeToMemory(blocks[0])) } if len(blocks) > 1 { for i := 1; i < len(blocks); i++ { intermediaCertPEM += string(pem.EncodeToMemory(blocks[i])) } } return serverCertPEM, intermediaCertPEM, nil } ================================================ FILE: pkg/utils/cert/hostname/hostname.go ================================================ package hostname import ( "crypto/x509" "net" "strings" ) // 检查目标主机名是否匹配待匹配主机名。 // // 入参: // - match: 待匹配主机名。可以是泛域名,如 "*.example.com"。 // - candidate: 目标主机名。如 "sub.example.com"。 // // 出参: // - 是否匹配。 func IsMatch(match, candidate string) bool { if match == "" || candidate == "" { return false } mockCert := &x509.Certificate{} if ip := net.ParseIP(match); ip != nil { mockCert.IPAddresses = []net.IP{ip} } else { if strings.EqualFold(match, candidate) { return true } mockCert.DNSNames = []string{match} } return mockCert.VerifyHostname(candidate) == nil } ================================================ FILE: pkg/utils/cert/hostname/hostname_test.go ================================================ package hostname_test import ( "testing" xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname" ) func TestCertHostnameUtil_IsMatch(t *testing.T) { t.Run("IsMatch", func(t *testing.T) { testCases := []struct { wildcard string target string expected bool }{ {"*.example.com", "sub.example.com", true}, {"*.example.com", "sub.sub.example.com", false}, {"*.example.com", "example.com", false}, {"*.*.example.com", "a.b.example.com", false}, {"*.*.example.com", "a.example.com", false}, {"*.*.example.com", "a.b.c.example.com", false}, {"example.com", "example.com", true}, {"example.com", "wrong.com", false}, {"", "example.com", false}, {"*.example.com", "", false}, {"*.sub.example.com", "a.sub.example.com", true}, {"*.sub.example.com", "a.b.sub.example.com", false}, {"*.sub.example.com", "sub.example.com", false}, {"*.Example.COM", "sub.example.com", true}, {"*.EXAMPLE.COM", "SUB.EXAMPLE.COM", true}, } for _, tc := range testCases { result := xcerthostname.IsMatch(tc.wildcard, tc.target) status := "✓" pf := t.Logf if result != tc.expected { status = "✗" pf = t.Errorf } pf("%s Wildcard: %-20s Target: %-20s Expected: %-5v Got: %-5v\n", status, tc.wildcard, tc.target, tc.expected, result) } }) } ================================================ FILE: pkg/utils/cert/key/key.go ================================================ package key import ( "crypto" "crypto/ecdsa" "crypto/ed25519" "crypto/rsa" "crypto/x509" "errors" ) type KeyAlgorithm = x509.PublicKeyAlgorithm func GetPublicKeyAlgorithm(pubkey crypto.PublicKey) (_algorithm KeyAlgorithm, _size int, _error error) { switch t := pubkey.(type) { case *rsa.PublicKey: size := t.N.BitLen() return x509.RSA, size, nil case *ecdsa.PublicKey: size := t.Curve.Params().BitSize return x509.ECDSA, size, nil case ed25519.PublicKey: return x509.Ed25519, 256, nil } return x509.UnknownPublicKeyAlgorithm, 0, errors.New("unknown public key type") } func GetPrivateKeyAlgorithm(privkey crypto.PrivateKey) (_algorithm KeyAlgorithm, _size int, _error error) { switch t := privkey.(type) { case *rsa.PrivateKey: size := t.N.BitLen() return x509.RSA, size, nil case *ecdsa.PrivateKey: size := t.Curve.Params().BitSize return x509.ECDSA, size, nil case ed25519.PrivateKey: return x509.Ed25519, 512, nil } return x509.UnknownPublicKeyAlgorithm, 0, errors.New("unknown private key type") } ================================================ FILE: pkg/utils/cert/parser.go ================================================ package cert import ( "crypto" "crypto/x509" "github.com/go-acme/lego/v4/certcrypto" ) // 从 PEM 编码的证书字符串解析并返回一个 x509.Certificate 对象。 // PEM 内容可能是包含多张证书的证书链,但只返回第一个证书(即服务器证书)。 // // 入参: // - certPEM: 证书 PEM 内容。 // // 出参: // - cert: x509.Certificate 对象。 // - err: 错误。 func ParseCertificateFromPEM(certPEM string) (_cert *x509.Certificate, _err error) { return certcrypto.ParsePEMCertificate([]byte(certPEM)) } // 从 PEM 编码的私钥字符串解析并返回一个 crypto.PrivateKey 对象。 // // 入参: // - privkeyPEM: 私钥 PEM 内容。 // // 出参: // - privkey: crypto.PrivateKey 对象,可能是 rsa.PrivateKey、ecdsa.PrivateKey 或 ed25519.PrivateKey。 // - err: 错误。 func ParsePrivateKeyFromPEM(privkeyPEM string) (_privkey crypto.PrivateKey, _err error) { return certcrypto.ParsePEMPrivateKey([]byte(privkeyPEM)) } ================================================ FILE: pkg/utils/cert/transformer.go ================================================ package cert import ( "bytes" "crypto/x509" "errors" "fmt" "time" "github.com/pavlo-v-chernykh/keystore-go/v4" "software.sslmate.com/src/go-pkcs12" ) // 将 PEM 编码的证书字符串转换为 PFX 格式。 // // 入参: // - certPEM: 证书 PEM 内容。 // - privkeyPEM: 私钥 PEM 内容。 // - pfxPassword: PFX 导出密码。 // // 出参: // - data: PFX 格式的证书数据。 // - err: 错误。 func TransformCertificateFromPEMToPFX(certPEM string, privkeyPEM string, pfxPassword string) ([]byte, error) { blocks := decodePEMBlocks([]byte(certPEM)) certs := make([]*x509.Certificate, 0, len(blocks)) for i, block := range blocks { if block.Type != "CERTIFICATE" { return nil, fmt.Errorf("invalid PEM block type at %d, expected 'CERTIFICATE', got '%s'", i, block.Type) } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return nil, err } certs = append(certs, cert) } privkey, err := ParsePrivateKeyFromPEM(privkeyPEM) if err != nil { return nil, err } var pfxData []byte if len(certs) == 0 { return nil, errors.New("failed to decode certificate PEM") } else if len(certs) == 1 { pfxData, err = pkcs12.Legacy.Encode(privkey, certs[0], nil, pfxPassword) } else { pfxData, err = pkcs12.Legacy.Encode(privkey, certs[0], certs[1:], pfxPassword) } return pfxData, err } // 将 PEM 编码的证书字符串转换为 JKS 格式。 // // 入参: // - certPEM: 证书 PEM 内容。 // - privkeyPEM: 私钥 PEM 内容。 // - jksAlias: JKS 别名。 // - jksKeypass: JKS 密钥密码。 // - jksStorepass: JKS 存储密码。 // // 出参: // - data: JKS 格式的证书数据。 // - err: 错误。 func TransformCertificateFromPEMToJKS(certPEM string, privkeyPEM string, jksAlias string, jksKeypass string, jksStorepass string) ([]byte, error) { certBlocks := decodePEMBlocks([]byte(certPEM)) if len(certBlocks) == 0 { return nil, errors.New("failed to decode certificate PEM") } privkeyBlocks := decodePEMBlocks([]byte(privkeyPEM)) if len(privkeyBlocks) == 0 { return nil, errors.New("failed to decode private key PEM") } entry := keystore.PrivateKeyEntry{ CreationTime: time.Now(), PrivateKey: privkeyBlocks[0].Bytes, CertificateChain: make([]keystore.Certificate, len(certBlocks)), } for i, certBlock := range certBlocks { if certBlock.Type != "CERTIFICATE" { return nil, fmt.Errorf("invalid PEM block type at %d, expected 'CERTIFICATE', got '%s'", i, certBlock.Type) } entry.CertificateChain[i] = keystore.Certificate{ Type: "X509", Content: certBlock.Bytes, } } ks := keystore.New() if err := ks.SetPrivateKeyEntry(jksAlias, entry, []byte(jksKeypass)); err != nil { return nil, err } var buf bytes.Buffer if err := ks.Store(&buf, []byte(jksStorepass)); err != nil { return nil, err } return buf.Bytes(), nil } ================================================ FILE: pkg/utils/cert/x509/x509.go ================================================ package x509 import ( "crypto/x509" "encoding/asn1" "net" ) var oidSubjectAlternativeNameExtension = asn1.ObjectIdentifier{2, 5, 29, 17} const ( sanGeneralNameTagEmail = 1 sanGeneralNameTagDNS = 2 sanGeneralNameTagURI = 6 sanGeneralNameTagIP = 7 ) // 返回指定 x509.Certificate 对象的主题名称。 // 如果主题名称为空,则返回第一个主题替代名称。 // // 入参: // - cert: x509.Certificate 对象。 // // 出参: // - 主题名称。 func GetSubjectCommonName(cert *x509.Certificate) string { if cert != nil { if cert.Subject.CommonName != "" { return cert.Subject.CommonName } sans := GetSubjectAltNames(cert) if len(sans) > 0 { return sans[0] } } return "" } // 返回指定 x509.Certificate 对象的主题替代名称。 // // 入参: // - cert: x509.Certificate 对象。 // // 出参: // - 主题替代名称的字符串切片。 func GetSubjectAltNames(cert *x509.Certificate) []string { sans := make([]string, 0) if cert != nil { // 注意,这里不直接使用 `DNSNames`、`IPAddresses` 等字段,以保证原始顺序不变 for _, ext := range cert.Extensions { if ext.Id.Equal(oidSubjectAlternativeNameExtension) { var seq asn1.RawValue if _, err := asn1.Unmarshal(ext.Value, &seq); err != nil { continue } rest := seq.Bytes for len(rest) > 0 { var name asn1.RawValue var err error rest, err = asn1.Unmarshal(rest, &name) if err != nil { break } switch name.Tag { case sanGeneralNameTagIP: var ip net.IP = name.Bytes sans = append(sans, ip.String()) case sanGeneralNameTagEmail, sanGeneralNameTagDNS, sanGeneralNameTagURI: sans = append(sans, string(name.Bytes)) default: // 忽略其他非 Critical 的 GeneralName​ } } } } } return sans } ================================================ FILE: pkg/utils/crypto/aes.go ================================================ package crypto import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/rand" "fmt" "io" ) type AESCryptor interface { CBCEncrypt(data []byte) ([]byte, error) CBCDecrypt(cipher []byte) ([]byte, error) } type aesCryptor struct { key []byte } func (c *aesCryptor) CBCEncrypt(data []byte) ([]byte, error) { block, err := aes.NewCipher(c.key) if err != nil { return nil, err } paddedData := c.pkcs7Padding(data, aes.BlockSize) ciphertext := make([]byte, aes.BlockSize+len(paddedData)) iv := ciphertext[:aes.BlockSize] if _, err := io.ReadFull(rand.Reader, iv); err != nil { return nil, err } mode := cipher.NewCBCEncrypter(block, iv) mode.CryptBlocks(ciphertext[aes.BlockSize:], paddedData) return ciphertext, nil } func (c *aesCryptor) CBCDecrypt(ciphertext []byte) ([]byte, error) { block, err := aes.NewCipher(c.key) if err != nil { return nil, err } if len(ciphertext) < aes.BlockSize { return nil, fmt.Errorf("ciphertext too short") } iv := ciphertext[:aes.BlockSize] ciphertext = ciphertext[aes.BlockSize:] if len(ciphertext)%aes.BlockSize != 0 { return nil, fmt.Errorf("ciphertext is not a multiple of the block size") } mode := cipher.NewCBCDecrypter(block, iv) mode.CryptBlocks(ciphertext, ciphertext) return c.pkcs7Unpadding(ciphertext), nil } func (c *aesCryptor) pkcs7Padding(data []byte, blockSize int) []byte { padding := blockSize - (len(data) % blockSize) padText := bytes.Repeat([]byte{byte(padding)}, padding) return append(data, padText...) } func (c *aesCryptor) pkcs7Unpadding(data []byte) []byte { length := len(data) if length == 0 { return data } padding := int(data[length-1]) if padding > length { return data } return data[:length-padding] } func NewAESCryptor(key []byte) AESCryptor { return &aesCryptor{key: key} } ================================================ FILE: pkg/utils/env/get.go ================================================ package env import ( "errors" "os" "strconv" ) // 以字符串形式读取指定环境变量的值。 // // 入参: // - envVar:环境变量。 // // 出参: // - 环境变量值。 func GetString(envVar string) string { return GetOrDefaultString(envVar, "") } // 以字符串形式读取指定环境变量的值。 // // 入参: // - envVar:环境变量。 // - defaultValue: 默认值。 // // 出参: // - 环境变量值。如果指定环境变量不存在、或者值为零值,则返回默认值。 func GetOrDefaultString(envVar, defaultValue string) string { return getOrDefault(envVar, defaultValue, parseString) } // 以整数形式读取指定环境变量的值。 // // 入参: // - envVar:环境变量。 // // 出参: // - 环境变量值。 func GetInt(envVar string) int { return GetOrDefaultInt(envVar, 0) } // 以整数形式读取指定环境变量的值。 // // 入参: // - envVar:环境变量。 // - defaultValue: 默认值。 // // 出参: // - 环境变量值。如果指定环境变量不存在、或者值的类型不是整数,则返回默认值。 func GetOrDefaultInt(envVar string, defaultValue int) int { return getOrDefault(envVar, defaultValue, strconv.Atoi) } // 以布尔形式读取指定环境变量的值。 // // 入参: // - envVar:环境变量。 // // 出参: // - 环境变量值。 func GetBool(envVar string) bool { return GetOrDefaultBool(envVar, false) } // 以布尔形式读取指定环境变量的值。 // // 入参: // - envVar:环境变量。 // - defaultValue: 默认值。 // // 出参: // - 环境变量值。如果指定环境变量不存在、或者值的类型不是布尔,则返回默认值。 func GetOrDefaultBool(envVar string, defaultValue bool) bool { return getOrDefault(envVar, defaultValue, strconv.ParseBool) } func getOrDefault[T any](envVar string, defaultValue T, parser func(string) (T, error)) T { v, err := parser(os.Getenv(envVar)) if err != nil { return defaultValue } return v } func parseString(s string) (string, error) { if s == "" { return "", errors.New("empty string") } return s, nil } ================================================ FILE: pkg/utils/file/io.go ================================================ package file import ( "fmt" "os" "path/filepath" ) // 与 [Write] 类似,但写入的是字符串内容。 // // 入参: // - path: 文件路径。 // - content: 文件内容。 // // 出参: // - 错误。 func WriteString(path string, content string) error { return Write(path, []byte(content)) } // 将数据写入指定路径的文件。 // 如果目录不存在,将会递归创建目录。 // 如果文件不存在,将会创建该文件;如果文件已存在,将会覆盖原有内容。 // // 入参: // - path: 文件路径。 // - data: 文件数据字节数组。 // // 出参: // - 错误。 func Write(path string, data []byte) error { dir := filepath.Dir(path) err := os.MkdirAll(dir, os.ModePerm) if err != nil { return fmt.Errorf("failed to create directory: %w", err) } file, err := os.Create(path) if err != nil { return fmt.Errorf("failed to create file: %w", err) } defer file.Close() _, err = file.Write(data) if err != nil { return fmt.Errorf("failed to write file: %w", err) } return nil } ================================================ FILE: pkg/utils/filepath/path.go ================================================ package filepath import ( stdfilepath "path/filepath" "strings" ) // 与标准库中的 [filepath.Dir] 类似,但会尝试保留原有的路径分隔符。 // // 入参: // - path: 文件路径。 // // 出参: // - 目录路径。 func Dir(path string) string { const SEP_WIN = "\\" const SEP_UNIX = "/" sep := SEP_UNIX if strings.Contains(path, SEP_WIN) && !strings.Contains(path, SEP_UNIX) { sep = SEP_WIN } dir := stdfilepath.Dir(path) if sep != SEP_UNIX && strings.Contains(dir, SEP_UNIX) { dir = strings.ReplaceAll(dir, SEP_UNIX, sep) } else if sep != SEP_WIN && strings.Contains(dir, SEP_WIN) { dir = strings.ReplaceAll(dir, SEP_WIN, sep) } return dir } ================================================ FILE: pkg/utils/http/parser.go ================================================ package http import ( "bufio" "net/http" "net/textproto" "strings" ) // 从表示 HTTP 标头的字符串解析并返回一个 http.Header 对象。 // // 入参: // - headers: 表示 HTTP 标头的字符串。 // // 出参: // - header: http.Header 对象。 // - err: 错误。 func ParseHeaders(headers string) (http.Header, error) { str := strings.TrimSpace(headers) + "\r\n\r\n" if len(str) == 4 { return make(http.Header), nil } br := bufio.NewReader(strings.NewReader(str)) tp := textproto.NewReader(br) mimeHeader, err := tp.ReadMIMEHeader() if err != nil { return nil, err } return http.Header(mimeHeader), err } ================================================ FILE: pkg/utils/http/transport.go ================================================ package http import ( "net" "net/http" "time" ) // 创建并返回一个 [http.DefaultTransport] 对象副本。 // // 出参: // - transport: [http.DefaultTransport] 对象副本。 func NewDefaultTransport() *http.Transport { if http.DefaultTransport != nil { if t, ok := http.DefaultTransport.(*http.Transport); ok { return t.Clone() } } return &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, DualStack: true, }).DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } } ================================================ FILE: pkg/utils/maps/get.go ================================================ package maps import ( "strconv" ) // 以字符串形式从字典中获取指定键的值。 // // 入参: // - dict: 字典。 // - key: 键。 // // 出参: // - 字典中键对应的值。如果指定键不存在、或者值的类型不是字符串,则返回空字符串。 func GetString(dict map[string]any, key string) string { return GetOrDefaultString(dict, key, "") } // 以字符串形式从字典中获取指定键的值。 // // 入参: // - dict: 字典。 // - key: 键。 // - defaultValue: 默认值。 // // 出参: // - 字典中键对应的值。如果指定键不存在、值的类型不是字符串、或者值为零值,则返回默认值。 func GetOrDefaultString(dict map[string]any, key string, defaultValue string) string { if dict == nil { return defaultValue } if value, ok := dict[key]; ok { if result, ok := value.(string); ok { if result != "" { return result } } } return defaultValue } // 以整数形式从字典中获取指定键的值。 // // 入参: // - dict: 字典。 // - key: 键。 // // 出参: // - 字典中键对应的值。如果指定键不存在、或者值的类型不是整数,则返回 0。 func GetInt(dict map[string]any, key string) int { return GetOrDefaultInt(dict, key, 0) } // 以整数形式从字典中获取指定键的值。 // // 入参: // - dict: 字典。 // - key: 键。 // - defaultValue: 默认值。 // // 出参: // - 字典中键对应的值。如果指定键不存在、值的类型不是整数、或者值为零值,则返回默认值。 func GetOrDefaultInt(dict map[string]any, key string, defaultValue int) int { if dict == nil { return defaultValue } if value, ok := dict[key]; ok { var result int switch v := value.(type) { case int: result = v case int8: result = int(v) case int16: result = int(v) case int32: result = int(v) case int64: result = int(v) case uint: result = int(v) case uint8: result = int(v) case uint16: result = int(v) case uint32: result = int(v) case uint64: result = int(v) case float32: result = int(v) case float64: result = int(v) case string: // 兼容字符串类型的值 if t, err := strconv.ParseInt(v, 10, 64); err == nil { result = int(t) } } if result != 0 { return result } } return defaultValue } // 以 32 位整数形式从字典中获取指定键的值。 // // 入参: // - dict: 字典。 // - key: 键。 // // 出参: // - 字典中键对应的值。如果指定键不存在、或者值的类型不是 32 位整数,则返回 0。 func GetInt32(dict map[string]any, key string) int32 { return GetOrDefaultInt32(dict, key, 0) } // 以 32 位整数形式从字典中获取指定键的值。 // // 入参: // - dict: 字典。 // - key: 键。 // - defaultValue: 默认值。 // // 出参: // - 字典中键对应的值。如果指定键不存在、值的类型不是 32 位整数、或者值为零值,则返回默认值。 func GetOrDefaultInt32(dict map[string]any, key string, defaultValue int32) int32 { if dict == nil { return defaultValue } if value, ok := dict[key]; ok { var result int32 switch v := value.(type) { case int: result = int32(v) case int8: result = int32(v) case int16: result = int32(v) case int32: result = v case int64: result = int32(v) case uint: result = int32(v) case uint8: result = int32(v) case uint16: result = int32(v) case uint32: result = int32(v) case uint64: result = int32(v) case float32: result = int32(v) case float64: result = int32(v) case string: // 兼容字符串类型的值 if t, err := strconv.ParseInt(v, 10, 32); err == nil { result = int32(t) } } if result != 0 { return result } } return defaultValue } // 以 64 位整数形式从字典中获取指定键的值。 // // 入参: // - dict: 字典。 // - key: 键。 // // 出参: // - 字典中键对应的值。如果指定键不存在、或者值的类型不是 64 位整数,则返回 0。 func GetInt64(dict map[string]any, key string) int64 { return GetOrDefaultInt64(dict, key, 0) } // 以 64 位整数形式从字典中获取指定键的值。 // // 入参: // - dict: 字典。 // - key: 键。 // - defaultValue: 默认值。 // // 出参: // - 字典中键对应的值。如果指定键不存在、值的类型不是 64 位整数、或者值为零值,则返回默认值。 func GetOrDefaultInt64(dict map[string]any, key string, defaultValue int64) int64 { if dict == nil { return defaultValue } if value, ok := dict[key]; ok { var result int64 switch v := value.(type) { case int: result = int64(v) case int8: result = int64(v) case int16: result = int64(v) case int32: result = int64(v) case int64: result = v case uint: result = int64(v) case uint8: result = int64(v) case uint16: result = int64(v) case uint32: result = int64(v) case uint64: result = int64(v) case float32: result = int64(v) case float64: result = int64(v) case string: // 兼容字符串类型的值 if t, err := strconv.ParseInt(v, 10, 64); err == nil { result = t } } if result != 0 { return result } } return defaultValue } // 以布尔形式从字典中获取指定键的值。 // // 入参: // - dict: 字典。 // - key: 键。 // // 出参: // - 字典中键对应的值。如果指定键不存在、或者值的类型不是布尔,则返回 false。 func GetBool(dict map[string]any, key string) bool { return GetOrDefaultBool(dict, key, false) } // 以布尔形式从字典中获取指定键的值。 // // 入参: // - dict: 字典。 // - key: 键。 // - defaultValue: 默认值。 // // 出参: // - 字典中键对应的值。如果指定键不存在、或者值的类型不是布尔,则返回默认值。 func GetOrDefaultBool(dict map[string]any, key string, defaultValue bool) bool { if dict == nil { return defaultValue } if value, ok := dict[key]; ok { if result, ok := value.(bool); ok { return result } // 兼容字符串类型的值 if str, ok := value.(string); ok { if result, err := strconv.ParseBool(str); err == nil { return result } } } return defaultValue } // 以 `map[string]V` 形式从字典中获取指定键的值。 // // 入参: // - dict: 字典。 // - key: 键。 // // 出参: // - 字典中键对应的 `map[string]V` 对象。 func GetKVMap[V any](dict map[string]any, key string) map[string]V { if dict == nil { return make(map[string]V) } if val, ok := dict[key]; ok { if result, ok := val.(map[string]V); ok { return result } } return make(map[string]V) } // 以 `map[string]any` 形式从字典中获取指定键的值。 // // 入参: // - dict: 字典。 // - key: 键。 // // 出参: // - 字典中键对应的 `map[string]any` 对象。 func GetKVMapAny(dict map[string]any, key string) map[string]any { return GetKVMap[any](dict, key) } ================================================ FILE: pkg/utils/maps/marshal.go ================================================ package maps import ( mapstructure "github.com/go-viper/mapstructure/v2" ) // 将字典填充到指定类型的结构体。 // 与 [json.Unmarshal] 类似,但传入的是一个 [map[string]any] 对象而非 JSON 格式的字符串。 // // 入参: // - dict: 字典。 // - output: 结构体指针。 // // 出参: // - 错误信息。如果填充失败,则返回错误信息。 func Populate(dict map[string]any, output any) error { config := &mapstructure.DecoderConfig{ Metadata: nil, Result: output, WeaklyTypedInput: true, TagName: "json", } decoder, err := mapstructure.NewDecoder(config) if err != nil { return err } return decoder.Decode(dict) } ================================================ FILE: pkg/utils/ssh/cmd.go ================================================ package ssh import ( "bytes" "fmt" "golang.org/x/crypto/ssh" ) // 执行远程脚本命令,并返回执行后标准输出和标准错误。 // // 入参: // - sshCli: SSH 客户端。 // - command: 待执行的脚本命令。 // // 出参: // - stdout:标准输出。 // - stderr:标准错误。 // - err: 错误。 func RunCommand(sshCli *ssh.Client, command string) (string, string, error) { session, err := sshCli.NewSession() if err != nil { return "", "", err } defer session.Close() stdoutBuf := bytes.NewBuffer(nil) session.Stdout = stdoutBuf stderrBuf := bytes.NewBuffer(nil) session.Stderr = stderrBuf err = session.Run(command) if err != nil { return stdoutBuf.String(), stderrBuf.String(), fmt.Errorf("failed to execute ssh command: %w", err) } return stdoutBuf.String(), stderrBuf.String(), nil } ================================================ FILE: pkg/utils/ssh/io.go ================================================ package ssh import ( "bytes" "errors" "fmt" "os" "github.com/pkg/sftp" "github.com/povsister/scp" "golang.org/x/crypto/ssh" xfilepath "github.com/certimate-go/certimate/pkg/utils/filepath" ) // 与 [WriteRemote] 类似,但写入的是字符串内容。 // // 入参: // - sshCli: SSH 客户端。 // - path: 文件远程路径。 // - data: 文件数据字节数组。 // - useSCP: 是否使用 SCP 进行传输,否则使用 SFTP。 // // 出参: // - 错误。 func WriteRemoteString(sshCli *ssh.Client, path string, content string, useSCP bool) error { if useSCP { return writeRemoteStringWithSCP(sshCli, path, content) } return writeRemoteStringWithSFTP(sshCli, path, content) } // 将数据写入指定远程路径的文件。 // 如果目录不存在,将会递归创建目录。 // 如果文件不存在,将会创建该文件;如果文件已存在,将会覆盖原有内容。 // // 入参: // - sshCli: SSH 客户端。 // - path: 文件远程路径。 // - data: 文件数据字节数组。 // - useSCP: 是否使用 SCP 进行传输,否则使用 SFTP。 // // 出参: // - 错误。 func WriteRemote(sshCli *ssh.Client, path string, data []byte, useSCP bool) error { if useSCP { return writeRemoteWithSCP(sshCli, path, data) } return writeRemoteWithSFTP(sshCli, path, data) } // 删除指定远程路径的文件。 // // 入参: // - sshCli: SSH 客户端。 // - path: 文件远程路径。 // - useSCP: 是否使用 SCP 进行传输,否则使用 SFTP。 // // 出参: // - 错误。 func RemoveRemote(sshCli *ssh.Client, path string, useSCP bool) error { if useSCP { return errors.ErrUnsupported } return removeRemoteWithSFTP(sshCli, path) } func writeRemoteStringWithSCP(sshCli *ssh.Client, path string, content string) error { return writeRemoteWithSCP(sshCli, path, []byte(content)) } func writeRemoteStringWithSFTP(sshCli *ssh.Client, path string, content string) error { return writeRemoteWithSFTP(sshCli, path, []byte(content)) } func writeRemoteWithSCP(sshCli *ssh.Client, path string, data []byte) error { scpCli, err := scp.NewClientFromExistingSSH(sshCli, &scp.ClientOption{}) if err != nil { return fmt.Errorf("failed to create scp client: %w", err) } reader := bytes.NewReader(data) err = scpCli.CopyToRemote(reader, path, &scp.FileTransferOption{}) if err != nil { return fmt.Errorf("failed to write to remote file: %w", err) } return nil } func writeRemoteWithSFTP(sshCli *ssh.Client, path string, data []byte) error { sftpCli, err := sftp.NewClient(sshCli) if err != nil { return fmt.Errorf("failed to create sftp client: %w", err) } defer sftpCli.Close() if err := sftpCli.MkdirAll(xfilepath.Dir(path)); err != nil { return fmt.Errorf("failed to create remote directory: %w", err) } file, err := sftpCli.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC) if err != nil { return fmt.Errorf("failed to open remote file: %w", err) } defer file.Close() _, err = file.Write(data) if err != nil { return fmt.Errorf("failed to write to remote file: %w", err) } return nil } func removeRemoteWithSFTP(sshCli *ssh.Client, path string) error { sftpCli, err := sftp.NewClient(sshCli) if err != nil { return fmt.Errorf("failed to create sftp client: %w", err) } defer sftpCli.Close() if err := sftpCli.MkdirAll(xfilepath.Dir(path)); err != nil { return fmt.Errorf("failed to create remote directory: %w", err) } if err := sftpCli.Remove(path); err != nil { return fmt.Errorf("failed to remove remote file: %w", err) } return nil } ================================================ FILE: pkg/utils/tls/config.go ================================================ package tls import ( "crypto/tls" ) // 创建并返回一个兼容低版的 [tls.Config] 对象。 // // 出参: // - config: [tls.Config] 对象。 func NewCompatibleConfig() *tls.Config { var suiteIds []uint16 for _, suite := range tls.CipherSuites() { suiteIds = append(suiteIds, suite.ID) } for _, suite := range tls.InsecureCipherSuites() { suiteIds = append(suiteIds, suite.ID) } return &tls.Config{ MinVersion: tls.VersionTLS10, CipherSuites: suiteIds, } } // 创建并返回一个不安全的 [tls.Config] 对象。 // // 出参: // - config: [tls.Config] 对象。 func NewInsecureConfig() *tls.Config { config := NewCompatibleConfig() config.InsecureSkipVerify = true return config } ================================================ FILE: pkg/utils/wait/delay.go ================================================ package wait import ( "context" "time" ) // 等待一段时间。 // // 入参: // - wait: 等待时间。 // // 出参: // - err: 错误。 func Delay(wait time.Duration) error { return DelayWithContext(context.Background(), wait) } // 等待一段时间,或上下文被取消。 // // 入参: // - ctx: 上下文。 // - wait: 等待时间。 // // 出参: // - err: 错误。 func DelayWithContext(ctx context.Context, wait time.Duration) error { ticker := time.NewTimer(wait) defer ticker.Stop() select { case <-ctx.Done(): return ctx.Err() case <-ticker.C: return nil } } ================================================ FILE: pkg/utils/wait/until.go ================================================ package wait import ( "context" "time" ) // 等待直到条件满足。 // // 入参: // - condition: 条件函数,接收尝试次数作为参数,返回是否满足条件和错误。 // - interval: 执行条件函数的间隔时间。 // // 出参: // - ret: 是否满足条件。 // - err: 错误。 func Until(condition func(index int) (bool, error), interval time.Duration) (bool, error) { conditionWithContext := func(_ context.Context, index int) (bool, error) { return condition(index) } return UntilWithContext(context.Background(), conditionWithContext, interval) } // 等待直到条件满足,或上下文被取消。 // // 入参: // - ctx: 上下文。 // - condition: 条件函数,接收上下文和尝试次数作为参数,返回是否满足条件和错误。 // - interval: 执行条件函数的间隔时间。 // // 出参: // - ret: 是否满足条件。 // - err: 错误。 func UntilWithContext(ctx context.Context, condition func(ctx context.Context, index int) (bool, error), interval time.Duration) (bool, error) { ticker := time.NewTicker(interval) defer ticker.Stop() attempt := 0 for { select { case <-ctx.Done(): return false, ctx.Err() case <-ticker.C: attempt++ ret, err := condition(ctx, attempt) if ret || err != nil { return ret, err } } } } // 等待直到条件满足或超时。 // // 入参: // - condition: 条件函数,接收尝试次数作为参数,返回是否满足条件和错误。 // - timeout: 超时时间。 // - interval: 执行条件函数的间隔时间。 // // 出参: // - ret: 是否满足条件。 // - err: 错误。 func UntilTimeout(condition func(index int) (bool, error), timeout time.Duration, interval time.Duration) (bool, error) { conditionWithContext := func(_ context.Context, index int) (bool, error) { return condition(index) } return UntilTimeoutWithContext(context.Background(), conditionWithContext, timeout, interval) } // 等待直到条件满足或超时,或上下文被取消。 // // 入参: // - ctx: 上下文。 // - condition: 条件函数,接收上下文和尝试次数作为参数,返回是否满足条件和错误。 // - timeout: 超时时间。 // - interval: 执行条件函数的间隔时间。 // // 出参: // - ret: 是否满足条件。 // - err: 错误。 func UntilTimeoutWithContext(ctx context.Context, condition func(ctx context.Context, index int) (bool, error), timeout time.Duration, interval time.Duration) (bool, error) { ctxWithTimeout, cancel := context.WithTimeout(ctx, timeout) defer cancel() ticker := time.NewTicker(interval) defer ticker.Stop() attempt := 0 for { select { case <-ctxWithTimeout.Done(): return false, ctx.Err() case <-ticker.C: attempt++ ret, err := condition(ctxWithTimeout, attempt) if ret || err != nil { return ret, err } } } } ================================================ FILE: ui/.gitignore ================================================ node_modules dist dist-ssr !dist/.gitkeep # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? *.local .env ================================================ FILE: ui/embed.go ================================================ // Package ui handles the PocketBase Admin frontend embedding. package ui import ( "embed" "github.com/pocketbase/pocketbase/apis" ) //go:embed all:dist var distDir embed.FS // DistDirFS contains the embedded dist directory files (without the "dist" prefix) var DistDirFS = apis.MustSubFS(distDir, "dist") ================================================ FILE: ui/eslint.config.mjs ================================================ import eslint from "@eslint/js"; import { defineConfig } from "eslint/config"; import tailwindcssPlugin from "eslint-plugin-better-tailwindcss"; import importPlugin from "eslint-plugin-import"; import prettierPluginConfig from "eslint-plugin-prettier/recommended"; import reactHooksPlugin from "eslint-plugin-react-hooks"; import reactRefreshPlugin from "eslint-plugin-react-refresh"; import typescriptPlugin from "typescript-eslint"; /** * @type {import("eslint").Linter.Config[]} */ export default defineConfig( // Basic eslint.configs["recommended"], { name: "eslint/import", extends: [importPlugin.flatConfigs["recommended"], importPlugin.flatConfigs["typescript"]], rules: { "import/no-named-as-default-member": "off", "import/no-unresolved": "off", "import/order": [ "error", { groups: ["builtin", "external", "internal", ["parent", "sibling"], "index"], pathGroups: [ { pattern: "react*", group: "external", position: "before", }, { pattern: "react/**", group: "external", position: "before", }, { pattern: "react-*", group: "external", position: "before", }, { pattern: "react-*/**", group: "external", position: "before", }, { pattern: "~/**", group: "external", position: "after", }, { pattern: "@/**", group: "internal", position: "before", }, ], pathGroupsExcludedImportTypes: ["builtin"], alphabetize: { order: "asc", caseInsensitive: true, }, }, ], "sort-imports": [ "error", { ignoreDeclarationSort: true, }, ], }, settings: { "import/resolver": { node: { extensions: [".js", ".jsx", ".ts", ".tsx"], }, typescript: { alwaysTryTypes: true, }, }, }, }, // Typescript { name: "typescript", extends: [typescriptPlugin.configs["recommended"]], rules: { "@typescript-eslint/consistent-type-imports": "error", "@typescript-eslint/no-empty-object-type": [ "error", { allowInterfaces: "with-single-extends", }, ], "@typescript-eslint/no-explicit-any": [ "warn", { ignoreRestArgs: true, }, ], "@typescript-eslint/no-unused-vars": [ "error", { argsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_", destructuredArrayIgnorePattern: "^_", varsIgnorePattern: "^_", }, ], }, }, // Pretter { name: "prettier", extends: [prettierPluginConfig], }, // React { name: "react", extends: [reactHooksPlugin.configs.flat["recommended-latest"], reactRefreshPlugin.configs["vite"]], rules: { "react-hooks/exhaustive-deps": ["warn"], "react-hooks/immutability": ["warn"], "react-hooks/refs": ["warn"], "react-hooks/preserve-manual-memoization": ["off"], "react-hooks/set-state-in-effect": ["warn"], "react-hooks/set-state-in-render": ["warn"], "react-refresh/only-export-components": [ "warn", { allowConstantExport: true, }, ], }, }, // TailwindCSS { name: "tailwindcss", plugins: { "better-tailwindcss": tailwindcssPlugin, }, rules: { ...tailwindcssPlugin.configs["recommended-warn"].rules, ...tailwindcssPlugin.configs["recommended-error"].rules, "better-tailwindcss/enforce-consistent-line-wrapping": "off", "better-tailwindcss/no-unknown-classes": "off", }, settings: { "better-tailwindcss": { entryPoint: "src/global.css", }, }, } ); ================================================ FILE: ui/index.html ================================================ Certimate - Your Trusted Partner in SSL Automation
================================================ FILE: ui/package.json ================================================ { "name": "@certimate/webui", "private": true, "type": "module", "scripts": { "dev": "vite --host", "build": "tsc -b && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, "dependencies": { "@codemirror/lang-json": "^6.0.2", "@codemirror/lang-yaml": "^6.1.2", "@codemirror/language": "^6.12.2", "@codemirror/legacy-modes": "^6.5.2", "@flowgram.ai/document": "1.0.8", "@flowgram.ai/fixed-layout-editor": "1.0.8", "@flowgram.ai/minimap-plugin": "1.0.8", "@peculiar/asn1-ecc": "^2.6.1", "@peculiar/asn1-pkcs8": "^2.6.1", "@peculiar/asn1-rsa": "^2.6.1", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/x509": "^1.14.3", "@tabler/icons-react": "^3.40.0", "@uiw/codemirror-extensions-basic-setup": "^4.25.8", "@uiw/codemirror-theme-vscode": "^4.25.8", "@uiw/react-codemirror": "^4.25.8", "ahooks": "^3.9.6", "antd": "^6.3.2", "antd-zod": "^8.0.0", "clsx": "^2.1.1", "cron-parser": "^5.5.0", "dayjs": "^1.11.20", "file-saver": "^2.0.5", "i18next": "^25.8.18", "i18next-browser-languagedetector": "^8.2.1", "immer": "^11.1.4", "nanoid": "^5.1.7", "pocketbase": "^0.26.8", "radash": "^12.1.1", "react": "^18.3.1", "react-copy-to-clipboard": "^5.1.1", "react-dom": "^18.3.1", "react-i18next": "^16.5.8", "react-router": "^7.13.1", "react-router-dom": "^7.13.1", "reflect-metadata": "^0.2.2", "tailwind-merge": "^3.5.0", "yaml": "^2.8.2", "zod": "^4.3.6", "zustand": "^5.0.12" }, "devDependencies": { "@eslint/js": "^9.39.2", "@tailwindcss/postcss": "^4.2.1", "@tailwindcss/vite": "^4.2.1", "@types/file-saver": "^2.0.7", "@types/fs-extra": "^11.0.4", "@types/node": "^24.10.4", "@types/react": "^18.3.26", "@types/react-copy-to-clipboard": "^5.0.7", "@types/react-dom": "^18.3.7", "@vitejs/plugin-legacy": "^7.2.1", "@vitejs/plugin-react": "^5.1.4", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-better-tailwindcss": "^4.3.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "fs-extra": "^11.3.4", "prettier": "^3.8.1", "tailwindcss": "^4.2.1", "typescript": "^5.9.3", "typescript-eslint": "^8.57.0", "vite": "^7.3.1" } } ================================================ FILE: ui/prettier.config.mjs ================================================ /** * @type {import("prettier").Config} */ export default { arrowParens: "always", bracketSpacing: true, editorconfig: true, htmlWhitespaceSensitivity: "ignore", jsxSingleQuote: false, endOfLine: "crlf", printWidth: 160, proseWrap: "preserve", quoteProps: "as-needed", semi: true, singleQuote: false, tabs: false, tabWidth: 2, trailingComma: "es5", useTabs: false, }; ================================================ FILE: ui/public/robots.txt ================================================ User-Agent: * Disallow: / ================================================ FILE: ui/src/App.tsx ================================================ import "reflect-metadata"; import { useEffect, useLayoutEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { RouterProvider } from "react-router-dom"; import { App, ConfigProvider, type ThemeConfig, theme } from "antd"; import { type Locale } from "antd/es/locale"; import AntdLocaleEnUS from "antd/locale/en_US"; import AntdLocaleZhCN from "antd/locale/zh_CN"; import dayjs from "dayjs"; import { z } from "zod"; import { en as ZodLocaleEnUs, zhCN as ZodLocaleZhCN } from "zod/locales"; import "dayjs/locale/zh-cn"; import { useBrowserTheme } from "@/hooks"; import { localeNames } from "@/i18n"; import { router } from "@/routers"; const antdLocalesMap: Record = { [localeNames.EN]: AntdLocaleEnUS, [localeNames.ZH]: AntdLocaleZhCN, }; const antdThemesMap: Record = { ["light"]: { algorithm: theme.defaultAlgorithm }, ["dark"]: { algorithm: theme.darkAlgorithm }, }; const zodLocalesMap: Record = { [localeNames.EN]: ZodLocaleEnUs, [localeNames.ZH]: ZodLocaleZhCN, }; const RootApp = () => { const { i18n } = useTranslation(); const { theme: browserTheme } = useBrowserTheme(); const [antdLocale, setAntdLocale] = useState(antdLocalesMap[i18n.language]); const [antdTheme, setAntdTheme] = useState(antdThemesMap[browserTheme]); const handleLanguageChanged = () => { setAntdLocale(antdLocalesMap[i18n.language]); dayjs.locale(i18n.language); z.config(zodLocalesMap[i18n.language]?.()); }; i18n.on("initialized", handleLanguageChanged); i18n.on("languageChanged", handleLanguageChanged); useLayoutEffect(() => { handleLanguageChanged(); return () => { i18n.off("initialized", handleLanguageChanged); i18n.off("languageChanged", handleLanguageChanged); }; }, [i18n]); useEffect(() => { setAntdTheme(antdThemesMap[browserTheme]); const root = window.document.documentElement; root.classList.remove("light", "dark"); root.classList.add(browserTheme); }, [browserTheme]); return ( ); }; export default RootApp; ================================================ FILE: ui/src/api/certificates.ts ================================================ import { ClientResponseError } from "pocketbase"; import { type CertificateFormatType } from "@/domain/certificate"; import { getPocketBase } from "@/repository/_pocketbase"; export const download = async (certificateId: string, format?: CertificateFormatType) => { const pb = getPocketBase(); type RespData = { fileBytes: string; }; const resp = await pb.send>(`/api/certificates/${encodeURIComponent(certificateId)}/download`, { method: "POST", headers: { "Content-Type": "application/json", }, body: { format: format, }, }); if (resp.code != 0) { throw new ClientResponseError({ status: resp.code, response: resp, data: {} }); } return resp; }; export const revoke = async (certificateId: string) => { const pb = getPocketBase(); const resp = await pb.send(`/api/certificates/${encodeURIComponent(certificateId)}/revoke`, { method: "POST", headers: { "Content-Type": "application/json", }, }); if (resp.code != 0) { throw new ClientResponseError({ status: resp.code, response: resp, data: {} }); } return resp; }; ================================================ FILE: ui/src/api/notifications.ts ================================================ import { ClientResponseError } from "pocketbase"; import { getPocketBase } from "@/repository/_pocketbase"; export const testPushNotification = async ({ provider, accessId }: { provider: string; accessId: string }) => { const pb = getPocketBase(); const resp = await pb.send("/api/notifications/test", { method: "POST", headers: { "Content-Type": "application/json", }, body: { provider, accessId, }, }); if (resp.code != 0) { throw new ClientResponseError({ status: resp.code, response: resp, data: {} }); } return resp; }; ================================================ FILE: ui/src/api/statistics.ts ================================================ import { ClientResponseError } from "pocketbase"; import { type Statistics } from "@/domain/statistics"; import { getPocketBase } from "@/repository/_pocketbase"; export const get = async () => { const pb = getPocketBase(); const resp = await pb.send>("/api/statistics", { method: "GET", }); if (resp.code != 0) { throw new ClientResponseError({ status: resp.code, response: resp, data: {} }); } return resp; }; ================================================ FILE: ui/src/api/workflows.ts ================================================ import { ClientResponseError } from "pocketbase"; import { WORKFLOW_TRIGGERS } from "@/domain/workflow"; import { getPocketBase } from "@/repository/_pocketbase"; export const getStats = async () => { const pb = getPocketBase(); type RespData = { concurrency: number; pendingRunIds: string[]; processingRunIds: string[]; }; const resp = await pb.send>(`/api/workflows/stats`, { method: "GET", headers: { "Content-Type": "application/json", }, }); if (resp.code != 0) { throw new ClientResponseError({ status: resp.code, response: resp, data: {} }); } return resp; }; export const startRun = async (workflowId: string) => { const pb = getPocketBase(); const resp = await pb.send(`/api/workflows/${encodeURIComponent(workflowId)}/runs`, { method: "POST", headers: { "Content-Type": "application/json", }, body: { trigger: WORKFLOW_TRIGGERS.MANUAL, }, }); if (resp.code != 0) { throw new ClientResponseError({ status: resp.code, response: resp, data: {} }); } return resp; }; export const cancelRun = async (workflowId: string, runId: string) => { const pb = getPocketBase(); const resp = await pb.send(`/api/workflows/${encodeURIComponent(workflowId)}/runs/${encodeURIComponent(runId)}/cancel`, { method: "POST", headers: { "Content-Type": "application/json", }, }); if (resp.code != 0) { throw new ClientResponseError({ status: resp.code, response: resp, data: {} }); } return resp; }; ================================================ FILE: ui/src/components/AppDocument.tsx ================================================ import { useTranslation } from "react-i18next"; import { IconBook } from "@tabler/icons-react"; import { Typography } from "antd"; import { APP_DOCUMENT_URL } from "@/domain/app"; export interface AppDocumentLinkButtonProps { className?: string; style?: React.CSSProperties; showIcon?: boolean; } const AppDocumentLinkButton = ({ className, style, showIcon = true }: AppDocumentLinkButtonProps) => { const { t } = useTranslation(); const handleDocumentClick = () => { window.open(APP_DOCUMENT_URL, "_blank"); }; return (
{showIcon ? : <>} {t("common.menu.document")}
); }; export default { LinkButton: AppDocumentLinkButton, }; ================================================ FILE: ui/src/components/AppLocale.tsx ================================================ import { useTranslation } from "react-i18next"; import { IconLanguage, type IconProps } from "@tabler/icons-react"; import { Dropdown, type DropdownProps, Typography } from "antd"; import { IconLanguageEnZh, IconLanguageZhEn } from "@/components/icons"; import Show from "@/components/Show"; import { localeNames, localeResources } from "@/i18n"; import { mergeCls } from "@/utils/css"; export const useAppLocaleMenuItems = () => { const { i18n } = useTranslation(); const items = Object.keys(i18n.store.data).map((key) => { return { key: key as string, label: i18n.store.data[key].name as string, onClick: () => { if (key !== (i18n.resolvedLanguage ?? i18n.language)) { i18n.changeLanguage(key); window.location.reload(); } }, }; }); return items; }; export interface AppLocaleDropdownProps { children?: React.ReactNode; trigger?: DropdownProps["trigger"]; } const AppLocaleDropdown = ({ children, trigger = ["click"] }: AppLocaleDropdownProps) => { const items = useAppLocaleMenuItems(); return ( {children} ); }; export interface AppLocaleIconProps extends IconProps {} const AppLocaleIcon = (props: AppLocaleIconProps) => { const { i18n } = useTranslation(); return ( ); }; export interface AppLocaleLinkButtonProps { className?: string; style?: React.CSSProperties; showIcon?: boolean; } const AppLocaleLinkButton = ({ className, style, showIcon = true }: AppLocaleLinkButtonProps) => { const { t } = useTranslation(); const { i18n } = useTranslation(); return (
{showIcon ? : <>} {String(localeResources[i18n.resolvedLanguage ?? i18n.language]?.name ?? t("common.menu.locale"))}
); }; export default { Dropdown: AppLocaleDropdown, Icon: AppLocaleIcon, LinkButton: AppLocaleLinkButton, }; ================================================ FILE: ui/src/components/AppTheme.tsx ================================================ import { useTranslation } from "react-i18next"; import { IconMoon, type IconProps, IconSun, IconSunMoon } from "@tabler/icons-react"; import { Dropdown, type DropdownProps, Typography } from "antd"; import Show from "@/components/Show"; import { useBrowserTheme } from "@/hooks"; import { mergeCls } from "@/utils/css"; export const useAppThemeMenuItems = () => { const { t } = useTranslation(); const { themeMode, setThemeMode } = useBrowserTheme(); const items = ( [ ["light", "common.theme.light", ], ["dark", "common.theme.dark", ], ["system", "common.theme.system", ], ] satisfies Array<[string, string, React.ReactNode]> ).map(([key, label, icon]) => { return { key: key, label: t(label), icon: icon, onClick: () => { if (key !== themeMode) { setThemeMode(key as Parameters[0]); window.location.reload(); } }, }; }); return items; }; export interface AppThemeDropdownProps { children?: React.ReactNode; trigger?: DropdownProps["trigger"]; } const AppThemeDropdown = ({ children, trigger = ["click"] }: AppThemeDropdownProps) => { const items = useAppThemeMenuItems(); return ( {children} ); }; export interface AppThemeIconProps extends IconProps {} const AppThemeIcon = (props: AppThemeIconProps) => { const { theme } = useBrowserTheme(); return ( ); }; export interface AppThemeLinkButtonProps { className?: string; style?: React.CSSProperties; showIcon?: boolean; } const AppThemeLinkButton = ({ className, style, showIcon = true }: AppThemeLinkButtonProps) => { const { t } = useTranslation(); const { themeMode } = useBrowserTheme(); return (
{showIcon ? : <>} {t(`common.theme.${themeMode}`)}
); }; export default { Dropdown: AppThemeDropdown, Icon: AppThemeIcon, LinkButton: AppThemeLinkButton, }; ================================================ FILE: ui/src/components/AppVersion.tsx ================================================ import { Badge, Typography } from "antd"; import { APP_DOWNLOAD_URL, APP_VERSION } from "@/domain/app"; import { useVersionChecker } from "@/hooks"; export interface AppVersionLinkButtonProps { className?: string; style?: React.CSSProperties; } const AppVersionLinkButton = ({ className, style }: AppVersionLinkButtonProps) => { return ( {APP_VERSION} ); }; export interface AppVersionBadgeProps { className?: string; style?: React.CSSProperties; children?: React.ReactNode; } const AppVersionBadge = ({ className, style, children }: AppVersionBadgeProps) => { const { hasUpdate } = useVersionChecker(); return ( {children} ); }; export default { LinkButton: AppVersionLinkButton, Badge: AppVersionBadge, }; ================================================ FILE: ui/src/components/CodeTextInput.tsx ================================================ import { useContext, useMemo, useRef } from "react"; import { json } from "@codemirror/lang-json"; import { yaml } from "@codemirror/lang-yaml"; import { StreamLanguage } from "@codemirror/language"; import { powerShell } from "@codemirror/legacy-modes/mode/powershell"; import { shell } from "@codemirror/legacy-modes/mode/shell"; import { basicSetup } from "@uiw/codemirror-extensions-basic-setup"; import { vscodeDark, vscodeLight } from "@uiw/codemirror-theme-vscode"; import CodeMirror, { EditorView, type ReactCodeMirrorProps, type ReactCodeMirrorRef } from "@uiw/react-codemirror"; import { useFocusWithin, useHover } from "ahooks"; import { theme } from "antd"; import DisabledContext from "antd/es/config-provider/DisabledContext"; import { useBrowserTheme } from "@/hooks"; import { mergeCls } from "@/utils/css"; export interface CodeTextInputProps extends Omit { disabled?: boolean; language?: string | string[]; lineNumbers?: boolean; lineWrapping?: boolean; readOnly?: boolean; } const CodeTextInput = ({ className, style, disabled, language, lineNumbers = true, lineWrapping = true, readOnly, ...props }: CodeTextInputProps) => { const { token: themeToken } = theme.useToken(); const { theme: browserTheme } = useBrowserTheme(); const injectedDisabled = useContext(DisabledContext); const mergedDisabled = disabled ?? injectedDisabled; const cmRef = useRef(null); const isFocusing = useFocusWithin(cmRef.current?.editor); const isHovering = useHover(cmRef.current?.editor); const cmTheme = useMemo(() => { if (browserTheme === "dark") { return vscodeDark; } return vscodeLight; }, [browserTheme]); const cmExtensions = useMemo(() => { const temp: NonNullable = [ basicSetup({ foldGutter: false, dropCursor: false, allowMultipleSelections: false, indentOnInput: false, }), ]; if (lineWrapping) { temp.push(EditorView.lineWrapping); } const langs = Array.isArray(language) ? language : [language]; langs.forEach((lang) => { switch (lang) { case "shell": temp.push(StreamLanguage.define(shell)); break; case "json": temp.push(json()); break; case "powershell": temp.push(StreamLanguage.define(powerShell)); break; case "yaml": temp.push(yaml()); break; } }); return temp; }, [language, lineWrapping]); return (
); }; export default CodeTextInput; ================================================ FILE: ui/src/components/CopyableText.tsx ================================================ import { CopyToClipboard } from "react-copy-to-clipboard"; import { useTranslation } from "react-i18next"; import { App, Button } from "antd"; export interface CopyableTextProps { className?: string; style?: React.CSSProperties; children?: React.ReactNode; text?: string; } const CopyableText = ({ className, style, children, text }: CopyableTextProps) => { const { t } = useTranslation(); const { message } = App.useApp(); return ( { message.success(t("common.text.copied")); }} > ); }; export default CopyableText; ================================================ FILE: ui/src/components/DrawerForm.tsx ================================================ import { useTranslation } from "react-i18next"; import { IconX } from "@tabler/icons-react"; import { useControllableValue } from "ahooks"; import { Button, Drawer, type DrawerProps, Flex, Form, type FormProps, type ModalProps } from "antd"; import { useAntdForm, useTriggerElement } from "@/hooks"; export interface DrawerFormProps = any> extends Omit, "title" | "onFinish"> { className?: string; style?: React.CSSProperties; children?: React.ReactNode; cancelButtonProps?: ModalProps["cancelButtonProps"]; cancelText?: ModalProps["cancelText"]; defaultOpen?: boolean; drawerProps?: Omit; okButtonProps?: ModalProps["okButtonProps"]; okText?: ModalProps["okText"]; open?: boolean; title?: React.ReactNode; trigger?: React.ReactNode; onFinish?: (values: T) => unknown | Promise; onOpenChange?: (open: boolean) => void; } const DrawerForm = = any>({ className, style, children, cancelText, cancelButtonProps, form, drawerProps, okText, okButtonProps, title, trigger, onFinish, ...props }: DrawerFormProps) => { const { t } = useTranslation(); const [open, setOpen] = useControllableValue(props, { valuePropName: "open", defaultValuePropName: "defaultOpen", trigger: "onOpenChange", }); const triggerEl = useTriggerElement(trigger, { onClick: () => { setOpen(true); }, }); const { form: formInst, formPending, formProps, submit: submitForm, } = useAntdForm({ form, onSubmit: (values) => { return onFinish?.(values); }, }); const mergedFormProps: FormProps = { clearOnDestroy: drawerProps?.destroyOnHidden ? true : void 0, ...formProps, ...props, }; const mergedDrawerProps: DrawerProps = { ...drawerProps, closeIcon: false, onClose: async (e) => { if (formPending) return; // 关闭 Drawer 时 Promise.reject 阻止关闭 await drawerProps?.onClose?.(e); setOpen(false); if (!mergedFormProps.preserve) { formInst.resetFields(); } }, }; const handleOkClick = async () => { // 提交表单返回 Promise.reject 时不关闭 Drawer await submitForm(); setOpen(false); }; const handleCancelClick = () => { if (formPending) return; setOpen(false); }; return ( <> {triggerEl} } forceRender open={open} title={
{title}
{mergedDrawerProps.closeIcon !== false && ( )} ); }; export default FileTextInput; ================================================ FILE: ui/src/components/ModalForm.tsx ================================================ import { useControllableValue } from "ahooks"; import { Form, type FormProps, Modal, type ModalProps } from "antd"; import { useAntdForm, useTriggerElement } from "@/hooks"; export interface ModalFormProps = any> extends Omit, "title" | "onFinish"> { className?: string; style?: React.CSSProperties; children?: React.ReactNode; cancelButtonProps?: ModalProps["cancelButtonProps"]; cancelText?: ModalProps["cancelText"]; defaultOpen?: boolean; modalProps?: Omit< ModalProps, | "cancelButtonProps" | "cancelText" | "confirmLoading" | "defaultOpen" | "forceRender" | "okButtonProps" | "okText" | "okType" | "open" | "title" | "width" | "onCancel" | "onOk" | "onOpenChange" >; okButtonProps?: ModalProps["okButtonProps"]; okText?: ModalProps["okText"]; open?: boolean; title?: ModalProps["title"]; trigger?: React.ReactNode; width?: ModalProps["width"]; onFinish?: (values: T) => unknown | Promise; onOpenChange?: (open: boolean) => void; } const ModalForm = = any>({ className, style, children, cancelButtonProps, cancelText, form, modalProps, okButtonProps, okText, title, trigger, width, onFinish, ...props }: ModalFormProps) => { const [open, setOpen] = useControllableValue(props, { valuePropName: "open", defaultValuePropName: "defaultOpen", trigger: "onOpenChange", }); const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) }); const { form: formInst, formPending, formProps, submit: submitForm, } = useAntdForm({ form, onSubmit: (values) => { return onFinish?.(values); }, }); const mergedFormProps: FormProps = { clearOnDestroy: modalProps?.destroyOnHidden ? true : void 0, ...formProps, ...props, }; const mergedModalProps: ModalProps = { ...modalProps, afterClose: () => { if (!mergedFormProps.preserve) { formInst.resetFields(); } modalProps?.afterClose?.(); }, }; const handleOkClick = async () => { // 提交表单返回 Promise.reject 时不关闭 Modal await submitForm(); setOpen(false); }; const handleCancelClick = () => { if (formPending) return; setOpen(false); }; return ( <> {triggerEl}
{children}
); }; export default ModalForm; ================================================ FILE: ui/src/components/MultipleInput.tsx ================================================ import { type ChangeEvent, forwardRef, useImperativeHandle, useMemo, useRef } from "react"; import { useTranslation } from "react-i18next"; import { IconCircleArrowDown, IconCircleArrowUp, IconCircleMinus, IconCirclePlus } from "@tabler/icons-react"; import { useControllableValue } from "ahooks"; import { Button, Input, type InputProps, type InputRef } from "antd"; import { produce } from "immer"; export interface MultipleInputProps extends Omit { allowClear?: boolean; defaultValue?: string[]; maxCount?: number; minCount?: number; showSortButton?: boolean; value?: string[]; onChange?: (value: string[]) => void; onValueChange?: (index: number, element: string) => void; onValueCreate?: (index: number) => void; onValueRemove?: (index: number) => void; onValueSort?: (oldIndex: number, newIndex: number) => void; } const MultipleInput = ({ allowClear = false, disabled, maxCount, minCount, showSortButton = true, onValueChange, onValueCreate, onValueSort, onValueRemove, ...props }: MultipleInputProps) => { const { t } = useTranslation(); const itemRefs = useRef([]); const [value, setValue] = useControllableValue(props, { valuePropName: "value", defaultValue: [], defaultValuePropName: "defaultValue", trigger: "onChange", }); const handleCreate = () => { const newValue = produce(value ?? [], (draft) => { draft.push(""); }); setValue(newValue); setTimeout(() => itemRefs.current[newValue.length - 1]?.focus(), 1); onValueCreate?.(newValue.length - 1); }; const handleChange = (index: number, element: string) => { const newValue = produce(value, (draft) => { draft[index] = element; }); setValue(newValue); onValueChange?.(index, element); }; const handleInputBlur = (index: number) => { if (!allowClear && !value[index]) { const newValue = produce(value, (draft) => { draft.splice(index, 1); }); setValue(newValue); } }; const handleClickUp = (index: number) => { if (index === 0) { return; } const newValue = produce(value, (draft) => { const temp = draft[index - 1]; draft[index - 1] = draft[index]; draft[index] = temp; }); setValue(newValue); onValueSort?.(index, index - 1); }; const handleClickDown = (index: number) => { if (index === value.length - 1) { return; } const newValue = produce(value, (draft) => { const temp = draft[index + 1]; draft[index + 1] = draft[index]; draft[index] = temp; }); setValue(newValue); onValueSort?.(index, index + 1); }; const handleClickAdd = (index: number) => { const newValue = produce(value, (draft) => { draft.splice(index + 1, 0, ""); }); setValue(newValue); setTimeout(() => itemRefs.current[index + 1]?.focus(), 1); onValueCreate?.(index + 1); }; const handleClickRemove = (index: number) => { const newValue = produce(value, (draft) => { draft.splice(index, 1); }); setValue(newValue); onValueRemove?.(index); }; return value == null || value.length === 0 ? ( ) : (
{Array.from(value).map((element, index) => { const allowUp = index > 0; const allowDown = index < value.length - 1; const allowRemove = minCount == null || value.length > minCount; const allowAdd = maxCount == null || value.length < maxCount; return ( (itemRefs.current[index] = ref!)} allowAdd={allowAdd} allowClear={allowClear} allowDown={allowDown} allowRemove={allowRemove} allowUp={allowUp} disabled={disabled} defaultValue={void 0} showSortButton={showSortButton} value={element} onBlur={() => handleInputBlur(index)} onChange={(val) => handleChange(index, val)} onEntryAdd={() => handleClickAdd(index)} onEntryDown={() => handleClickDown(index)} onEntryUp={() => handleClickUp(index)} onEntryRemove={() => handleClickRemove(index)} /> ); })}
); }; type MultipleInputItemProps = Omit< MultipleInputProps, "defaultValue" | "maxCount" | "minCount" | "preset" | "value" | "onChange" | "onValueCreate" | "onValueRemove" | "onValueSort" | "onValueChange" > & { allowAdd: boolean; allowRemove: boolean; allowUp: boolean; allowDown: boolean; defaultValue?: string; value?: string; onChange?: (value: string) => void; onEntryAdd?: () => void; onEntryDown?: () => void; onEntryUp?: () => void; onEntryRemove?: () => void; }; type MultipleInputItemInstance = { focus: InputRef["focus"]; blur: InputRef["blur"]; select: InputRef["select"]; }; const MultipleInputItem = forwardRef( ( { allowAdd, allowClear, allowDown, allowRemove, allowUp, disabled, showSortButton, onEntryAdd, onEntryDown, onEntryUp, onEntryRemove, ...props }: MultipleInputItemProps, ref ) => { const inputRef = useRef(null); const [value, setValue] = useControllableValue(props, { valuePropName: "value", defaultValue: "", defaultValuePropName: "defaultValue", trigger: "onChange", }); const upBtn = useMemo(() => { if (!showSortButton) return null; return } validateTrigger="onSubmit" width={modalWidth} > ); }; export default MultipleSplitValueInput; ================================================ FILE: ui/src/components/Show.tsx ================================================ import { Children as ReactChildren, isValidElement } from "react"; export type ShowProps = | { children: React.ReactNode; } | { when: boolean; children: React.ReactNode; fallback?: React.ReactNode; }; const Show = ({ children, ...props }: ShowProps) => { if ("when" in props) { const { when, fallback } = props; return when ? children : fallback; } let fallback: React.ReactNode | undefined; const cases = ReactChildren.toArray(children); for (let i = 0; i < cases.length; i++) { const child = cases[i]; if (isValidElement(child)) { if (child.type === Case && child.props.when) { return child.props.children; } else if (child.type === Default) { if (fallback) { console.warn("[certimate] multiple Default components found in Show. Only the first will be used."); continue; } fallback = child.props.children; } } } return fallback; }; const Case = ({ children, when }: { children: React.ReactNode; when: boolean }) => { return when ? children : null; }; const Default = ({ children }: { children: React.ReactNode }) => { return children; }; const _default = Object.assign(Show, { Case, Default, }); export default _default; ================================================ FILE: ui/src/components/Tips.tsx ================================================ import { IconBulb } from "@tabler/icons-react"; import { Alert, Flex, Typography, theme } from "antd"; export interface TipsProps { className?: string; style?: React.CSSProperties; message: React.ReactNode; } const Tips = ({ className, style, message }: TipsProps) => { const { token: themeToken } = theme.useToken(); return (
{message}
} type="info" /> ); }; export default Tips; ================================================ FILE: ui/src/components/access/AccessEditDrawer.tsx ================================================ import { startTransition, useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; import { IconChevronDown, IconX } from "@tabler/icons-react"; import { useControllableValue, useGetState } from "ahooks"; import { App, Button, Drawer, Dropdown, Flex, Form, Space } from "antd"; import { testPushNotification } from "@/api/notifications"; import AccessProviderPicker from "@/components/provider/AccessProviderPicker"; import Show from "@/components/Show"; import { type AccessModel } from "@/domain/access"; import { ACCESS_USAGES } from "@/domain/provider"; import { useTriggerElement, useZustandShallowSelector } from "@/hooks"; import { useAccessesStore } from "@/stores/access"; import { unwrapErrMsg } from "@/utils/error"; import AccessForm, { type AccessFormModes, type AccessFormProps, type AccessFormUsages } from "./AccessForm"; export interface AccessEditDrawerProps { afterClose?: () => void; afterSubmit?: (record: AccessModel) => void; data?: AccessFormProps["initialValues"]; loading?: boolean; mode: AccessFormModes; open?: boolean; trigger?: React.ReactNode; usage?: AccessFormUsages; onOpenChange?: (open: boolean) => void; } const AccessEditDrawer = ({ afterSubmit, mode, data, loading, trigger, usage, ...props }: AccessEditDrawerProps) => { const { t } = useTranslation(); const { message, notification } = App.useApp(); const { createAccess, updateAccess } = useAccessesStore(useZustandShallowSelector(["createAccess", "updateAccess"])); const [open, setOpen] = useControllableValue(props, { valuePropName: "open", defaultValuePropName: "defaultOpen", trigger: "onOpenChange", }); const afterClose = () => { setFormPending(false); setFormChanged(false); setIsTesting(false); props.afterClose?.(); }; const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) }); const providerFilter = AccessForm.useProviderFilterByUsage(usage); const [formInst] = Form.useForm(); const [formPending, setFormPending] = useState(false); const [formChanged, setFormChanged] = useState(false); const submitForm = async () => { let formValues: AccessModel; setFormPending(true); try { formValues = await formInst.validateFields(); formValues.reserve = usage === "ca" ? "ca" : usage === "notification" ? "notif" : void 0; } catch (err) { message.warning(t("common.errmsg.form_invalid")); setFormPending(false); throw err; } try { switch (mode) { case "create": { if (data?.id) { throw "Invalid props: `data`"; } formValues = await createAccess(formValues); } break; case "modify": { if (!data?.id) { throw "Invalid props: `data`"; } formValues = await updateAccess({ ...data, ...formValues }); } break; default: throw "Invalid props: `mode`"; } afterSubmit?.(formValues); } catch (err) { notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); throw err; } finally { setFormPending(false); } }; const fieldProvider = Form.useWatch("provider", { form: formInst, preserve: true }); const [isTesting, setIsTesting] = useState(false); const handleProviderPick = (value: string) => { formInst.setFieldValue("provider", value); }; const handleFormChange = () => { setFormChanged(true); }; const handleOkClick = async () => { await submitForm(); setOpen(false); }; const handleOkAndContinueClick = async () => { await submitForm(); message.success(t("common.text.saved")); }; const handleCancelClick = () => { if (formPending) return; setOpen(false); }; const handleTestPushClick = async () => { setIsTesting(true); try { await formInst.validateFields(); } catch { setIsTesting(false); return; } try { await testPushNotification({ provider: fieldProvider, accessId: data!.id }); message.success(t("common.text.operation_succeeded")); } catch (err) { notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); } finally { setIsTesting(false); } }; return ( <> {triggerEl} !open && afterClose?.()} autoFocus closeIcon={false} destroyOnHidden footer={ fieldProvider ? ( {usage === "notification" ? ( ) : ( {/* TODO: 测试连接 */} )} )} ); }; const getInitialValues = (): Nullish>> => { return { host: "127.0.0.1", port: 22, authMethod: AUTH_METHOD_PASSWORD, username: "root", }; }; const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { const { t } = i18n; const baseSchema = z .object({ host: z.string().refine((v) => isHostname(v), t("common.errmsg.host_invalid")), port: z.coerce.number().refine((v) => isPortNumber(v), t("common.errmsg.port_invalid")), authMethod: z.literal([AUTH_METHOD_NONE, AUTH_METHOD_PASSWORD, AUTH_METHOD_KEY], t("access.form.ssh_auth_method.placeholder")), username: z.string().nonempty(t("access.form.ssh_username.placeholder")), password: z.string().nullish(), key: z .string() .max(20480, t("common.errmsg.string_max", { max: 20480 })) .nullish(), keyPassphrase: z.string().nullish(), }) .superRefine((values, ctx) => { switch (values.authMethod) { case AUTH_METHOD_PASSWORD: { if (!values.password?.trim()) { ctx.addIssue({ code: "custom", message: t("access.form.ssh_password.placeholder"), path: ["password"], }); } } break; case AUTH_METHOD_KEY: { if (!values.key?.trim()) { ctx.addIssue({ code: "custom", message: t("access.form.ssh_key.placeholder"), path: ["key"], }); } } break; } }); return baseSchema.safeExtend({ jumpServers: z .array(baseSchema, t("access.form.ssh_jump_servers.errmsg.invalid")) .nullish() .refine((v) => { if (v == null) return true; return v.every((item) => baseSchema.safeParse(item).success); }, t("access.form.ssh_jump_servers.errmsg.invalid")), }); }; const _default = Object.assign(AccessConfigFormFieldsProviderSSH, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/access/forms/AccessConfigFieldsProviderSSLCom.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Tips from "@/components/Tips"; import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderSSLCom = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } /> ); }; const getInitialValues = (): Nullish>> => { return { eabKid: "", eabHmacKey: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { const { t } = i18n; return z.object({ eabKid: z.string().nonempty(t("access.form.shared_acme_eab_kid.placeholder")), eabHmacKey: z.string().nonempty(t("access.form.shared_acme_eab_hmac_key.placeholder")), }); }; const _default = Object.assign(AccessConfigFormFieldsProviderSSLCom, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/access/forms/AccessConfigFieldsProviderSafeLine.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Switch } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderSafeLine = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { serverUrl: "http://:9443/", apiToken: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { const { t } = i18n; return z.object({ serverUrl: z.url(t("common.errmsg.url_invalid")), apiToken: z.string().nonempty(t("access.form.safeline_api_token.placeholder")), allowInsecureConnections: z.boolean().nullish(), }); }; const _default = Object.assign(AccessConfigFormFieldsProviderSafeLine, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/access/forms/AccessConfigFieldsProviderSectigo.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Tips from "@/components/Tips"; import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderSectigo = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } /> ); }; const getInitialValues = (): Nullish>> => { return { validationType: "dv", eabKid: "", eabHmacKey: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { const { t } = i18n; return z.object({ validationType: z.string().nonempty(t("access.form.sectigo_validation_type.placeholder")), eabKid: z.string().nonempty(t("access.form.shared_acme_eab_kid.placeholder")), eabHmacKey: z.string().nonempty(t("access.form.shared_acme_eab_hmac_key.placeholder")), }); }; const _default = Object.assign(AccessConfigFormFieldsProviderSectigo, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/access/forms/AccessConfigFieldsProviderSlackBot.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderSlackBot = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { botToken: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { const { t } = i18n; return z.object({ botToken: z.string().nonempty(t("access.form.slackbot_token.placeholder")), channelId: z.string().nullish(), }); }; const _default = Object.assign(AccessConfigFormFieldsProviderSlackBot, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/access/forms/AccessConfigFieldsProviderSpaceship.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderSpaceship = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { apiKey: "", apiSecret: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { const { t } = i18n; return z.object({ apiKey: z.string().nonempty(t("access.form.spaceship_api_key.placeholder")), apiSecret: z.string().nonempty(t("access.form.spaceship_api_secret.placeholder")), }); }; const _default = Object.assign(AccessConfigFormFieldsProviderSpaceship, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/access/forms/AccessConfigFieldsProviderSynologyDSM.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Switch } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFieldsProviderSynologyDSM = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { serverUrl: "http://:5000/", username: "", password: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { const { t } = i18n; return z.object({ serverUrl: z.url(t("common.errmsg.url_invalid")), username: z.string().nonempty(t("access.form.synologydsm_username.placeholder")), password: z.string().nonempty(t("access.form.synologydsm_password.placeholder")), totpSecret: z .string() .nullish() .refine((v) => { if (!v) return true; return /^[A-Z2-7]{16,32}$/.test(v); }), allowInsecureConnections: z.boolean().nullish(), }); }; const _default = Object.assign(AccessConfigFieldsProviderSynologyDSM, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/access/forms/AccessConfigFieldsProviderTechnitiumDNS.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Switch } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderTechnitiumDNS = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { serverUrl: "http://:5380/", apiToken: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { const { t } = i18n; return z.object({ serverUrl: z.url(t("common.errmsg.url_invalid")), apiToken: z.string().nonempty(t("access.form.technitiumdns_api_token.placeholder")), allowInsecureConnections: z.boolean().nullish(), }); }; const _default = Object.assign(AccessConfigFormFieldsProviderTechnitiumDNS, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/access/forms/AccessConfigFieldsProviderTelegramBot.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderTelegramBot = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { botToken: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { const { t } = i18n; return z.object({ botToken: z.string().nonempty(t("access.form.telegrambot_token.placeholder")), chatId: z .preprocess( (v) => (v == null || v === "" ? void 0 : Number(v)), z.number().refine((v) => { return !Number.isNaN(+v!) && +v! !== 0; }, t("access.form.telegrambot_chat_id.placeholder")) ) .nullish(), }); }; const _default = Object.assign(AccessConfigFormFieldsProviderTelegramBot, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/access/forms/AccessConfigFieldsProviderTencentCloud.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderTencentCloud = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { secretId: "", secretKey: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { const { t } = i18n; return z.object({ secretId: z.string().nonempty(t("access.form.tencentcloud_secret_id.placeholder")), secretKey: z.string().nonempty(t("access.form.tencentcloud_secret_key.placeholder")), }); }; const _default = Object.assign(AccessConfigFormFieldsProviderTencentCloud, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/access/forms/AccessConfigFieldsProviderTodayNIC.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Tips from "@/components/Tips"; import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderTodayNIC = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } /> ); }; const getInitialValues = (): Nullish>> => { return { userId: "", apiKey: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { const { t } = i18n; return z.object({ userId: z.string().nonempty(t("access.form.todaynic_user_id.placeholder")), apiKey: z.string().nonempty(t("access.form.todaynic_api_key.placeholder")), }); }; const _default = Object.assign(AccessConfigFormFieldsProviderTodayNIC, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/access/forms/AccessConfigFieldsProviderUCloud.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderUCloud = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > } > } > ); }; const getInitialValues = (): Nullish>> => { return { privateKey: "", publicKey: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { const { t } = i18n; return z.object({ privateKey: z.string().nonempty(t("access.form.ucloud_private_key.placeholder")), publicKey: z.string().nonempty(t("access.form.ucloud_public_key.placeholder")), projectId: z.string().nullish(), }); }; const _default = Object.assign(AccessConfigFormFieldsProviderUCloud, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/access/forms/AccessConfigFieldsProviderUniCloud.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderUniCloud = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { username: "", password: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { const { t } = i18n; return z.object({ username: z.string().nonempty(t("access.form.unicloud_username.placeholder")), password: z.string().nonempty(t("access.form.unicloud_password.placeholder")), }); }; const _default = Object.assign(AccessConfigFormFieldsProviderUniCloud, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/access/forms/AccessConfigFieldsProviderUpyun.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderUpyun = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { username: "", password: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { const { t } = i18n; return z.object({ username: z.string().nonempty(t("access.form.upyun_username.placeholder")), password: z.string().nonempty(t("access.form.upyun_password.placeholder")), }); }; const _default = Object.assign(AccessConfigFormFieldsProviderUpyun, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/access/forms/AccessConfigFieldsProviderVercel.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderVercel = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { apiAccessToken: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { const { t } = i18n; return z.object({ apiAccessToken: z.string().nonempty(t("access.form.vercel_api_access_token.placeholder")), teamId: z.string().nullish(), }); }; const _default = Object.assign(AccessConfigFormFieldsProviderVercel, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/access/forms/AccessConfigFieldsProviderVolcEngine.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderVolcEngine = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { accessKeyId: "", secretAccessKey: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { const { t } = i18n; return z.object({ accessKeyId: z.string().nonempty(t("access.form.volcengine_access_key_id.placeholder")), secretAccessKey: z.string().nonempty(t("access.form.volcengine_secret_access_key.placeholder")), }); }; const _default = Object.assign(AccessConfigFormFieldsProviderVolcEngine, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/access/forms/AccessConfigFieldsProviderVultr.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderVultr = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { apiKey: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { const { t } = i18n; return z.object({ apiKey: z.string().nonempty(t("access.form.vultr_api_key.placeholder")), }); }; const _default = Object.assign(AccessConfigFormFieldsProviderVultr, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/access/forms/AccessConfigFieldsProviderWangsu.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderWangsu = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > } > } > ); }; const getInitialValues = (): Nullish>> => { return { accessKeyId: "", accessKeySecret: "", apiKey: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { const { t } = i18n; return z.object({ accessKeyId: z.string().nonempty(t("access.form.wangsu_access_key_id.placeholder")), accessKeySecret: z.string().nonempty(t("access.form.wangsu_access_key_secret.placeholder")), apiKey: z.string().nonempty(t("access.form.wangsu_api_key.placeholder")), }); }; const _default = Object.assign(AccessConfigFormFieldsProviderWangsu, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/access/forms/AccessConfigFieldsProviderWeComBot.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Checkbox, Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import CodeTextInput from "@/components/CodeTextInput"; import { isJsonObject } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderWeComBot = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance>(); const initialValues = getInitialValues(); const fieldUseCustomPayload = Form.useWatch([parentNamePath, "useCustomPayload"], formInst); const handleCustomPayloadChecked = (checked: boolean) => { formInst.setFieldValue([parentNamePath, "useCustomPayload"], checked); if (checked) { formInst.setFieldValue([parentNamePath, "customPayload"], commonPayloadString); } else { formInst.setFieldValue([parentNamePath, "customPayload"], void 0); } }; const handleCustomPayloadBlur = () => { const value = formInst.getFieldValue([parentNamePath, "customPayload"]); try { const json = JSON.stringify(JSON.parse(value), null, 2); formInst.setFieldValue([parentNamePath, "customPayload"], json); } catch { return; } }; return ( <> } > handleCustomPayloadChecked(e.target.checked)}> {t("access.form.wecombot_custom_payload.checkbox")} ); }; const getInitialValues = (): Nullish>> => { return { webhookUrl: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { const { t } = i18n; return z .object({ webhookUrl: z.url(t("common.errmsg.url_invalid")), useCustomPayload: z.boolean().nullish(), customPayload: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.useCustomPayload) { if (!isJsonObject(values.customPayload!)) { ctx.addIssue({ code: "custom", message: t("common.errmsg.json_invalid"), path: ["customPayload"], }); } } }); }; const commonPayloadString = JSON.stringify( { msgtype: "text", text: { content: "${CERTIMATE_NOTIFIER_SUBJECT}\n\n${CERTIMATE_NOTIFIER_MESSAGE}", }, }, null, 2 ); const _default = Object.assign(AccessConfigFormFieldsProviderWeComBot, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/access/forms/AccessConfigFieldsProviderWebhook.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { IconChevronDown } from "@tabler/icons-react"; import { Button, Dropdown, Form, Input, Select, Switch } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import CodeTextInput from "@/components/CodeTextInput"; import Show from "@/components/Show"; import Tips from "@/components/Tips"; import { isJsonObject } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; export interface AccessConfigFormFieldsWebhookProps { usage?: "deployment" | "notification" | "none"; } const AccessConfigFormFieldsProviderWebhook = ({ usage = "none" }: AccessConfigFormFieldsWebhookProps) => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues({ usage }); const handleWebhookHeadersBlur = () => { let value = formInst.getFieldValue([parentNamePath, "headers"]); value = value.trim(); value = value.replace(/(? { const value = formInst.getFieldValue([parentNamePath, "data"]); try { const json = JSON.stringify(JSON.parse(value), null, 2); formInst.setFieldValue([parentNamePath, "data"], json); } catch { return; } }; const handleWebhookDataForNotificationBlur = () => { const value = formInst.getFieldValue([parentNamePath, "data"]); try { const json = JSON.stringify(JSON.parse(value), null, 2); formInst.setFieldValue([parentNamePath, "data"], json); } catch { return; } }; const handlePresetDataForDeploymentClick = () => { formInst.setFieldValue([parentNamePath, "method"], "POST"); formInst.setFieldValue([parentNamePath, "headers"], "Content-Type: application/json"); formInst.setFieldValue([parentNamePath, "data"], getInitialValues({ usage: "deployment" }).data); }; const handlePresetDataForNotificationClick = (key: string) => { switch (key) { case "bark": formInst.setFieldValue([parentNamePath, "url"], "https://api.day.app/push"); formInst.setFieldValue([parentNamePath, "method"], "POST"); formInst.setFieldValue([parentNamePath, "headers"], "Content-Type: application/json"); formInst.setFieldValue( [parentNamePath, "data"], JSON.stringify( { title: "${CERTIMATE_NOTIFIER_SUBJECT}", body: "${CERTIMATE_NOTIFIER_MESSAGE}", device_key: "", }, null, 2 ) ); break; case "gotify": formInst.setFieldValue([parentNamePath, "url"], "https:///"); formInst.setFieldValue([parentNamePath, "method"], "POST"); formInst.setFieldValue([parentNamePath, "headers"], "Content-Type: application/json\r\nAuthorization: Bearer "); formInst.setFieldValue( [parentNamePath, "data"], JSON.stringify( { title: "${CERTIMATE_NOTIFIER_SUBJECT}", message: "${CERTIMATE_NOTIFIER_MESSAGE}", priority: 1, }, null, 2 ) ); break; case "messagenest": formInst.setFieldValue([parentNamePath, "url"], "http:///api/v1/message/send"); formInst.setFieldValue([parentNamePath, "method"], "POST"); formInst.setFieldValue([parentNamePath, "headers"], "Content-Type: application/json"); formInst.setFieldValue( [parentNamePath, "data"], JSON.stringify( { token: "", title: "${CERTIMATE_NOTIFIER_SUBJECT}", text: "${CERTIMATE_NOTIFIER_MESSAGE}", }, null, 2 ) ); break; case "ntfy": formInst.setFieldValue([parentNamePath, "url"], "https:///"); formInst.setFieldValue([parentNamePath, "method"], "POST"); formInst.setFieldValue([parentNamePath, "headers"], "Content-Type: application/json"); formInst.setFieldValue( [parentNamePath, "data"], JSON.stringify( { topic: "", title: "${CERTIMATE_NOTIFIER_SUBJECT}", message: "${CERTIMATE_NOTIFIER_MESSAGE}", priority: 1, }, null, 2 ) ); break; case "pushme": formInst.setFieldValue([parentNamePath, "url"], "https://push.i-i.me/"); formInst.setFieldValue([parentNamePath, "method"], "POST"); formInst.setFieldValue([parentNamePath, "headers"], "Content-Type: application/json"); formInst.setFieldValue( [parentNamePath, "data"], JSON.stringify( { push_key: "", type: "text", title: "${CERTIMATE_NOTIFIER_SUBJECT}", content: "${CERTIMATE_NOTIFIER_MESSAGE}", }, null, 2 ) ); break; case "pushover": formInst.setFieldValue([parentNamePath, "url"], "https://api.pushover.net/1/messages.json"); formInst.setFieldValue([parentNamePath, "method"], "POST"); formInst.setFieldValue([parentNamePath, "headers"], "Content-Type: application/json"); formInst.setFieldValue( [parentNamePath, "data"], JSON.stringify( { token: "", user: "", title: "${CERTIMATE_NOTIFIER_SUBJECT}", message: "${CERTIMATE_NOTIFIER_MESSAGE}", }, null, 2 ) ); break; case "pushplus": formInst.setFieldValue([parentNamePath, "url"], "https://www.pushplus.plus/send"); formInst.setFieldValue([parentNamePath, "method"], "POST"); formInst.setFieldValue([parentNamePath, "headers"], "Content-Type: application/json"); formInst.setFieldValue( [parentNamePath, "data"], JSON.stringify( { token: "", title: "${CERTIMATE_NOTIFIER_SUBJECT}", content: "${CERTIMATE_NOTIFIER_MESSAGE}", }, null, 2 ) ); break; case "serverchan3": formInst.setFieldValue([parentNamePath, "url"], "https://.push.ft07.com/send/.send"); formInst.setFieldValue([parentNamePath, "method"], "POST"); formInst.setFieldValue([parentNamePath, "headers"], "Content-Type: application/json"); formInst.setFieldValue( [parentNamePath, "data"], JSON.stringify( { title: "${CERTIMATE_NOTIFIER_SUBJECT}", desp: "${CERTIMATE_NOTIFIER_MESSAGE}", }, null, 2 ) ); break; case "serverchanturbo": formInst.setFieldValue([parentNamePath, "url"], "https://sctapi.ftqq.com/.send"); formInst.setFieldValue([parentNamePath, "method"], "POST"); formInst.setFieldValue([parentNamePath, "headers"], "Content-Type: application/json"); formInst.setFieldValue( [parentNamePath, "data"], JSON.stringify( { title: "${CERTIMATE_NOTIFIER_SUBJECT}", desp: "${CERTIMATE_NOTIFIER_MESSAGE}", }, null, 2 ) ); break; case "wxpush": formInst.setFieldValue([parentNamePath, "url"], "http:///wxsend"); formInst.setFieldValue([parentNamePath, "method"], "POST"); formInst.setFieldValue([parentNamePath, "headers"], "Content-Type: application/json\r\nAuthorization: "); formInst.setFieldValue( [parentNamePath, "data"], JSON.stringify( { title: "${CERTIMATE_NOTIFIER_SUBJECT}", content: "${CERTIMATE_NOTIFIER_MESSAGE}", }, null, 2 ) ); break; default: formInst.setFieldValue([parentNamePath, "method"], "POST"); formInst.setFieldValue([parentNamePath, "headers"], "Content-Type: application/json"); formInst.setFieldValue([parentNamePath, "data"], getInitialValues({ usage: "notification" }).data); break; } }; return ( <> } /> ); }; const getInitialValues = (): Nullish>> => { return { username: "", apiPassword: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { const { t } = i18n; return z.object({ username: z.string().nonempty(t("access.form.westcn_username.placeholder")), apiPassword: z.string().nonempty(t("access.form.westcn_api_password.placeholder")), }); }; const _default = Object.assign(AccessConfigFormFieldsProviderWestcn, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/access/forms/AccessConfigFieldsProviderXinnet.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Tips from "@/components/Tips"; import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderXinnet = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } /> ); }; const getInitialValues = (): Nullish>> => { return { agentId: "", apiPassword: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { const { t } = i18n; return z.object({ agentId: z.string().nonempty(t("access.form.xinnet_agent_id.placeholder")), apiPassword: z.string().nonempty(t("access.form.xinnet_api_password.placeholder")), }); }; const _default = Object.assign(AccessConfigFormFieldsProviderXinnet, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/access/forms/AccessConfigFieldsProviderZeroSSL.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Tips from "@/components/Tips"; import { useFormNestedFieldsContext } from "./_context"; const AccessConfigFormFieldsProviderZeroSSL = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } /> ); }; const getInitialValues = (): Nullish>> => { return { eabKid: "", eabHmacKey: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { const { t } = i18n; return z.object({ eabKid: z.string().nonempty(t("access.form.shared_acme_eab_kid.placeholder")), eabHmacKey: z.string().nonempty(t("access.form.shared_acme_eab_hmac_key.placeholder")), }); }; const _default = Object.assign(AccessConfigFormFieldsProviderZeroSSL, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/access/forms/_context.ts ================================================ import { createContext, useContext } from "react"; // #region FormNestedFieldsContext export type FormNestedFieldsContextType = { parentNamePath: string; }; export const FormNestedFieldsContext = createContext({ parentNamePath: "", }); export const FormNestedFieldsContextProvider = FormNestedFieldsContext.Provider; export const useFormNestedFieldsContext = () => { const context = useContext(FormNestedFieldsContext); if (!context) { throw new Error("`FormNestedFieldsContext` must be used within a `FormNestedFieldsContextProvider`"); } return context; }; // #endregion ================================================ FILE: ui/src/components/access/forms/_hooks.ts ================================================ import { useMemo } from "react"; import { ACCESS_USAGES, type AccessProvider } from "@/domain/provider"; export const useProviderFilterByUsage = (usage?: "dns" | "hosting" | "dns-hosting" | "ca" | "notification") => { return useMemo(() => { if (usage == null) return; switch (usage) { case "dns": return (_: string, option: AccessProvider) => option.usages.includes(ACCESS_USAGES.DNS); case "hosting": return (_: string, option: AccessProvider) => option.usages.includes(ACCESS_USAGES.HOSTING); case "dns-hosting": return (_: string, option: AccessProvider) => option.usages.includes(ACCESS_USAGES.DNS) || option.usages.includes(ACCESS_USAGES.HOSTING); case "ca": return (_: string, option: AccessProvider) => option.usages.includes(ACCESS_USAGES.CA); case "notification": return (_: string, option: AccessProvider) => option.usages.includes(ACCESS_USAGES.NOTIFICATION); default: console.warn(`[certimate] unsupported provider usage: '${usage}'`); } }, [usage]); }; ================================================ FILE: ui/src/components/certificate/CertificateDetail.tsx ================================================ import { CopyToClipboard } from "react-copy-to-clipboard"; import { useTranslation } from "react-i18next"; import { IconClipboard, IconDownload, IconThumbUp } from "@tabler/icons-react"; import { App, Button, Dropdown, Form, Input, Tag, Tooltip } from "antd"; import dayjs from "dayjs"; import { saveAs } from "file-saver"; import { download as downloadCertificate } from "@/api/certificates"; import { CERTIFICATE_FORMATS, type CertificateFormatType, type CertificateModel } from "@/domain/certificate"; export interface CertificateDetailProps { className?: string; style?: React.CSSProperties; data: CertificateModel; } const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => { const { t } = useTranslation(); const { message } = App.useApp(); const handleDownloadClick = async (format: CertificateFormatType) => { try { const res = await downloadCertificate(data.id, format); 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`); } catch (err) { console.error(err); message.warning(t("common.text.operation_failed")); } }; return (
{t("certificate.props.revoked")} : <>} />
{ message.success(t("common.text.copied")); }} >
{ message.success(t("common.text.copied")); }} >
, onClick: () => handleDownloadClick(CERTIFICATE_FORMATS.PEM), }, { key: "PFX", label: "PFX", onClick: () => handleDownloadClick(CERTIFICATE_FORMATS.PFX), }, { key: "JKS", label: "JKS", onClick: () => handleDownloadClick(CERTIFICATE_FORMATS.JKS), }, ], }} trigger={["click", "hover"]} >
); }; export default CertificateDetail; ================================================ FILE: ui/src/components/certificate/CertificateDetailDrawer.tsx ================================================ import { startTransition, useCallback, useState } from "react"; import { IconX } from "@tabler/icons-react"; import { useControllableValue, useGetState } from "ahooks"; import { Button, Drawer, Flex } from "antd"; import Show from "@/components/Show"; import { type CertificateModel } from "@/domain/certificate"; import { useTriggerElement } from "@/hooks"; import CertificateDetail from "./CertificateDetail"; export interface CertificateDetailDrawerProps { afterClose?: () => void; data?: CertificateModel; loading?: boolean; open?: boolean; trigger?: React.ReactNode; onOpenChange?: (open: boolean) => void; } const CertificateDetailDrawer = ({ afterClose, data, loading, trigger, ...props }: CertificateDetailDrawerProps) => { const [open, setOpen] = useControllableValue(props, { valuePropName: "open", defaultValuePropName: "defaultOpen", trigger: "onOpenChange", }); const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) }); return ( <> {triggerEl} !open && afterClose?.()} autoFocus closeIcon={false} destroyOnHidden open={open} loading={loading} placement="right" size="large" title={
{data ? `Certificate #${data.id}` : "Certificate"}
} open={open} title={t("workflow.detail.design.action.import.modal.title")} width="768px" onCancel={handleCancelClick} >
); }; const useModal = () => { const [open, setOpen] = useState(false); const [onOkHandler, setOnOkHandler] = useState<{ handler: WorkflowGraphImportModalProps["onOk"] }>(); const onOpenChange = useCallback((open: boolean) => { setOpen(open); }, []); return { modalProps: { afterClose: () => { startTransition(() => { if (!open) { setOnOkHandler(void 0); } }); }, open, onOk: (graph: WorkflowGraph) => { onOkHandler?.handler?.(graph); }, onOpenChange, }, open: () => { setOpen(true); const { promise, resolve } = Promise.withResolvers(); setOnOkHandler({ handler: (graph) => resolve(graph) }); return promise; }, close: () => { setOpen(false); }, }; }; const _default = Object.assign(WorkflowGraphImportModal, { useModal, }); export default _default; ================================================ FILE: ui/src/components/workflow/WorkflowRunDetail.tsx ================================================ import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { EditorState, FlowLayoutDefault } from "@flowgram.ai/fixed-layout-editor"; import { IconBrowserShare, IconBug, IconCheck, IconDots, IconDownload, IconSettings2, IconTransferOut } from "@tabler/icons-react"; import { useRequest } from "ahooks"; import { Alert, App, Button, Card, Divider, Dropdown, Empty, Skeleton, Table, type TableProps, Tooltip, Typography, theme } 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 { WorkflowLogLevel, type WorkflowLogModel } from "@/domain/workflowLog"; import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun"; import { useBrowserTheme } from "@/hooks"; import { listByWorkflowRunId as listCertificatesByWorkflowRunId } from "@/repository/certificate"; import { listByWorkflowRunId as listLogsByWorkflowRunId } from "@/repository/workflowLog"; import { subscribe as subscribeWorkflowRun } from "@/repository/workflowRun"; import { mergeCls } from "@/utils/css"; import { unwrapErrMsg } from "@/utils/error"; import WorkflowDesigner from "./designer/Designer"; import WorkflowToolbar from "./designer/Toolbar"; import WorkflowGraphExportModal from "./WorkflowGraphExportModal"; import WorkflowStatus from "./WorkflowStatus"; export interface WorkflowRunDetailProps { className?: string; style?: React.CSSProperties; data: WorkflowRunModel; } const WorkflowRunDetail = ({ className, style, ...props }: WorkflowRunDetailProps) => { const { t } = useTranslation(); const [innerData, setInnerData] = useState(props.data); const mergedData = useMemo(() => ({ ...props.data, ...innerData }), [innerData, props.data]); const unsubscriberRef = useRef<() => void>(); useEffect(() => { if (props.data.status === WORKFLOW_RUN_STATUSES.PENDING || props.data.status === WORKFLOW_RUN_STATUSES.PROCESSING) { subscribeWorkflowRun(props.data.id, (cb) => { setInnerData(cb.record); if (cb.record.status !== WORKFLOW_RUN_STATUSES.PENDING && cb.record.status !== WORKFLOW_RUN_STATUSES.PROCESSING) { unsubscriberRef.current?.(); unsubscriberRef.current = undefined; } }).then((unsubscriber) => { unsubscriberRef.current = unsubscriber; }); } return () => { unsubscriberRef.current?.(); unsubscriberRef.current = undefined; }; }, [props.data.id, props.data.status]); return (
{mergedData.endedAt ? t("workflow_run.base.description_with_time_cost", { trigger: t(`workflow_run.base.trigger.${mergedData.trigger}`), startedAt: dayjs(mergedData.startedAt).format("YYYY-MM-DD HH:mm:ss"), timeCost: dayjs(mergedData.endedAt).diff(dayjs(mergedData.startedAt), "second") + "s", }) : t("workflow_run.base.description", { trigger: t(`workflow_run.base.trigger.${mergedData.trigger}`), startedAt: dayjs(mergedData.startedAt).format("YYYY-MM-DD HH:mm:ss"), })}
} type={ { [WORKFLOW_RUN_STATUSES.SUCCEEDED]: "success" as const, [WORKFLOW_RUN_STATUSES.FAILED]: "error" as const, [WORKFLOW_RUN_STATUSES.CANCELED]: "warning" as const, }[mergedData.status] ?? ("info" as const) } /> {!!mergedData.error && ( } showIcon title={
{mergedData.error}
} /> )}
{t("workflow_run.process")}
{t("workflow_run.logs")}
0}>
{t("workflow_run.artifacts")}
); }; const WorkflowRunProcess = ({ runData }: { runData: WorkflowRunModel }) => { const { t } = useTranslation(); const { token: themeToken } = theme.useToken(); const { modalProps: graphExportModalProps, ...graphExportModal } = WorkflowGraphExportModal.useModal(); const handleExportClick = () => { graphExportModal.open({ data: runData.graph! }); }; return ( <>
, onClick: handleExportClick, }, ], }} trigger={["click"]} >
); }; const WorkflowRunLogs = ({ runData }: { runData: WorkflowRunModel }) => { const { t } = useTranslation(); const { theme: browserTheme } = useBrowserTheme(); const { id: runId, status: runStatus } = runData; type Log = Pick; type LogGroup = { id: string; name: string; records: Log[] }; const [listData, setListData] = useState([]); const { loading, ...req } = useRequest( () => { return listLogsByWorkflowRunId(runId); }, { refreshDeps: [runId, runStatus], pollingInterval: 1000, pollingWhenHidden: false, throttleWait: 500, onSuccess: (res) => { if (res.items.length === listData.flatMap((e) => e.records).length) return; setListData( res.items.reduce((acc, e) => { let group = acc.at(-1); if (!group || group.id !== e.nodeId) { group = { id: e.nodeId, name: e.nodeName, records: [] }; acc.push(group); } group.records.push({ timestamp: e.timestamp, level: e.level, message: e.message, data: e.data }); return acc; }, [] as LogGroup[]) ); }, onFinally: () => { if (runStatus !== WORKFLOW_RUN_STATUSES.PENDING && runStatus !== WORKFLOW_RUN_STATUSES.PROCESSING) { req.cancel(); } }, onError: (err) => { if (err instanceof ClientResponseError && err.isAbort) { return; } console.error(err); throw err; }, } ); const [showTimestamp, setShowTimestamp] = useState(true); const [showWhitespace, setShowWhitespace] = useState(true); const renderLogRecord = (record: Log) => { let timestamp = dayjs(record.timestamp).format("YYYY-MM-DD HH:mm:ss"); timestamp = `[${timestamp}]`; let message = <>{record.message}; if (record.data != null && Object.keys(record.data).length > 0) { message = (
{record.message} {Object.entries(record.data).map(([key, value]) => (
{key}:
{JSON.stringify(value)}
))}
); } return (
{showTimestamp &&
{timestamp}
}
{message}
); }; const handleDownloadClick = () => { const NEWLINE = "\n"; const logstr = listData .map((group) => { const escape = (str: string) => str.replaceAll("\r", "\\r").replaceAll("\n", "\\n"); return ( `#${group.id} ${group.name}` + NEWLINE + group.records .map((record) => { const datetime = dayjs(record.timestamp).format("YYYY-MM-DDTHH:mm:ss.SSSZ"); const level = record.level < WorkflowLogLevel.Info ? "DBUG" : record.level < WorkflowLogLevel.Warn ? "INFO" : record.level < WorkflowLogLevel.Error ? "WARN" : "ERRO"; const message = record.message; const data = record.data && Object.keys(record.data).length > 0 ? JSON.stringify(record.data) : ""; return `[${datetime}] [${level}] ${escape(message)} ${escape(data)}`.trim(); }) .join(NEWLINE) ); }) .join(NEWLINE + NEWLINE); const blob = new Blob([logstr], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `certimate_workflow_run_#${runId}_logs.txt`; a.click(); URL.revokeObjectURL(url); a.remove(); }; return (
, onClick: () => setShowTimestamp(!showTimestamp), }, { key: "show-whitespace", label: t("workflow_run.logs.menu.show_whitespaces"), icon: , onClick: () => setShowWhitespace(!showWhitespace), }, { type: "divider", }, { key: "download-logs", label: t("workflow_run.logs.menu.download_logs"), icon: , onClick: handleDownloadClick, }, ], }} trigger={["click"]} >
0} fallback={}> {listData.map((group, index) => { return (
{`#${group.id}\u00A0`} {group.name}
{group.records.map((record) => renderLogRecord(record))}
); })}
); }; const WorkflowRunArtifacts = ({ runData }: { runData: WorkflowRunModel }) => { const { t } = useTranslation(); const { notification } = App.useApp(); const { id: runId } = runData; 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"), render: (_, record) => { return (
{record.subjectAltNames}
); }, }, { key: "$action", align: "end", width: 32, render: (_, record) => (
), }, ]; const [tableData, setTableData] = useState([]); const { loading } = useRequest( () => { // TODO: 目前输出产物只有证书 return listCertificatesByWorkflowRunId(runId); }, { refreshDeps: [runId], onSuccess: (res) => { setTableData(res.items); }, onError: (err) => { if (err instanceof ClientResponseError && err.isAbort) { return; } console.error(err); notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); throw err; }, } ); return ( columns={tableColumns} dataSource={tableData} loading={loading} locale={{ emptyText: , }} pagination={false} rowKey={(record) => record.id} size="small" /> ); }; export default WorkflowRunDetail; ================================================ FILE: ui/src/components/workflow/WorkflowRunDetailDrawer.tsx ================================================ import { startTransition, useCallback, useState } from "react"; import { IconX } from "@tabler/icons-react"; import { useControllableValue, useGetState } from "ahooks"; import { Button, Drawer, Flex } from "antd"; import Show from "@/components/Show"; import { type WorkflowRunModel } from "@/domain/workflowRun"; import { useTriggerElement } from "@/hooks"; import WorkflowRunDetail from "./WorkflowRunDetail"; export interface WorkflowRunDetailDrawerProps { afterClose?: () => void; data?: WorkflowRunModel; loading?: boolean; open?: boolean; trigger?: React.ReactNode; onOpenChange?: (open: boolean) => void; } const WorkflowRunDetailDrawer = ({ afterClose, data, loading, trigger, ...props }: WorkflowRunDetailDrawerProps) => { const [open, setOpen] = useControllableValue(props, { valuePropName: "open", defaultValuePropName: "defaultOpen", trigger: "onOpenChange", }); const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) }); return ( <> {triggerEl} !open && afterClose?.()} closeIcon={false} destroyOnHidden open={open} loading={loading} placement="right" size="large" title={
{data ? `Workflow Run #${data.id}` : "Workflow Run"}
); }; export default BranchAdder; ================================================ FILE: ui/src/components/workflow/designer/elements/Collapse.tsx ================================================ import { type CollapseProps as FlowgramCollapseProps } from "@flowgram.ai/fixed-layout-editor"; export interface CollapseProps extends FlowgramCollapseProps {} const Collapse = (_: CollapseProps) => { return null; }; export default Collapse; ================================================ FILE: ui/src/components/workflow/designer/elements/DragHighlightAdder.tsx ================================================ import { type DragNodeProps as FlowgramDragNodeProps } from "@flowgram.ai/fixed-layout-editor"; import { IconGradienter } from "@tabler/icons-react"; const DragHighlightAdder = (_: FlowgramDragNodeProps) => { return (
); }; export default DragHighlightAdder; ================================================ FILE: ui/src/components/workflow/designer/elements/DragNode.tsx ================================================ import { type FlowNodeEntity, type DragNodeProps as FlowgramDragNodeProps } from "@flowgram.ai/fixed-layout-editor"; import { Badge, Card } from "antd"; export interface DragNodeProps extends FlowgramDragNodeProps { dragStart: FlowNodeEntity; dragNodes: FlowNodeEntity[]; } const DragNode = ({ dragStart, dragNodes }: DragNodeProps) => { const count = (dragNodes || []) .map((n) => (n.allCollapsedChildren.length ? n.allCollapsedChildren.filter((_n) => !_n.hidden).length : 1)) .reduce((acc, cur) => acc + cur, 0); return ( 1 ? count : 0} size="small">
{dragStart ? dragStart.form?.getValueIn("name") || `#${dragStart?.id}` : "\u00A0"}
); }; export default DragNode; ================================================ FILE: ui/src/components/workflow/designer/elements/DraggingAdder.tsx ================================================ import { FlowDragLayer, type AdderProps as FlowgramAdderProps, usePlayground } from "@flowgram.ai/fixed-layout-editor"; import { IconChevronsDown } from "@tabler/icons-react"; export interface DraggingAdderProps extends FlowgramAdderProps {} const DraggingAdder = ({ from }: DraggingAdderProps) => { const playground = usePlayground(); const layer = playground.getLayer(FlowDragLayer); if (!layer) return <>; if ( layer.options.canDrop && !layer.options.canDrop({ dragNodes: layer.dragEntities ?? [], dropNode: from, isBranch: false, }) ) { return <>; } return (
); }; export default DraggingAdder; ================================================ FILE: ui/src/components/workflow/designer/elements/Null.tsx ================================================ import { type FlowNodeEntity } from "@flowgram.ai/fixed-layout-editor"; export interface NullProps { node: FlowNodeEntity; } const Null = (_: NullProps) => { return null; }; export default Null; ================================================ FILE: ui/src/components/workflow/designer/elements/TryCatchCollapse.tsx ================================================ import { type CustomLabelProps, FlowNodeRenderData, FlowNodeTransformData, FlowRendererRegistry, FlowTextKey, useBaseColor, } from "@flowgram.ai/fixed-layout-editor"; export interface TryCatchCollapseProps extends CustomLabelProps {} const TryCatchCollapse = ({ node, ...props }: TryCatchCollapseProps) => { const { baseColor, baseActivatedColor } = useBaseColor(); const nodeRenderData = node.getData(FlowNodeRenderData)!; const nodeTransformData = node.getData(FlowNodeTransformData)!; const handleMouseEnter = () => { nodeRenderData.activated = true; }; const handleMouseLeave = () => { nodeRenderData.activated = false; }; if (!nodeTransformData || !nodeTransformData.parent) { return <>; } const width = nodeTransformData.inputPoint.x - nodeTransformData.parent.inputPoint.x; const height = 40; return (
{node.getService(FlowRendererRegistry).getText(FlowTextKey.CATCH_TEXT)}
); }; export default TryCatchCollapse; ================================================ FILE: ui/src/components/workflow/designer/elements/index.ts ================================================ import { FlowRendererKey } from "@flowgram.ai/fixed-layout-editor"; import Adder from "./Adder"; import BranchAdder from "./BranchAdder"; import Collapse from "./Collapse"; import DraggingAdder from "./DraggingAdder"; import DragHighlightAdder from "./DragHighlightAdder"; import DragNode from "./DragNode"; import Null from "./Null"; import TryCatchCollapse from "./TryCatchCollapse"; export const getAllElements = () => { return { [FlowRendererKey.ADDER]: Adder, [FlowRendererKey.BRANCH_ADDER]: BranchAdder, [FlowRendererKey.SLOT_ADDER]: Null, [FlowRendererKey.COLLAPSE]: Collapse, [FlowRendererKey.TRY_CATCH_COLLAPSE]: TryCatchCollapse, [FlowRendererKey.SLOT_COLLAPSE]: Null, [FlowRendererKey.DRAG_NODE]: DragNode, [FlowRendererKey.DRAG_HIGHLIGHT_ADDER]: DragHighlightAdder, [FlowRendererKey.DRAG_BRANCH_HIGHLIGHT_ADDER]: DragHighlightAdder, [FlowRendererKey.DRAGGABLE_ADDER]: DraggingAdder, [FlowRendererKey.SELECTOR_BOX_POPOVER]: Null, }; }; ================================================ FILE: ui/src/components/workflow/designer/flowgram.css ================================================ /* 隐藏 flowgram 的标签背景 */ /* flowgram 暂未提供相关配置项,这里需与 antd.colorBgContainer 保持一致 */ :root { --g-editor-background: #fff; } .dark { --g-editor-background: #262c2d; } /* 隐藏 flowgram 的折叠按钮 */ .flow-canvas-collapse-adder > .flow-canvas-collapse { display: hidden !important; position: absolute !important; top: -99999px !important; left: -99999px !important; width: 0 !important; height: 0 !important; visibility: hidden !important; transform: scale(0) !important; } /* 控制 flowgram 线条样式 */ /* flowgram 暂未提供相关配置项 */ .flow-lines-container { stroke-width: 1.5px; } ================================================ FILE: ui/src/components/workflow/designer/forms/BizApplyNodeConfigDrawer.tsx ================================================ import { useTranslation } from "react-i18next"; import { type FlowNodeEntity } from "@flowgram.ai/fixed-layout-editor"; import { Form } from "antd"; import { type WorkflowNodeConfigForBizApply } from "@/domain/workflow"; import { NodeConfigDrawer } from "./_shared"; import BizApplyNodeConfigForm from "./BizApplyNodeConfigForm"; import { NodeType } from "../nodes/typings"; export interface BizApplyNodeConfigDrawerProps { afterClose?: () => void; loading?: boolean; node: FlowNodeEntity; open?: boolean; onOpenChange?: (open: boolean) => void; } const BizApplyNodeConfigDrawer = ({ node, ...props }: BizApplyNodeConfigDrawerProps) => { if (node.flowNodeType !== NodeType.BizApply) { console.warn(`[certimate] current workflow node type is not: ${NodeType.BizApply}`); } const { i18n } = useTranslation(); const [formInst] = Form.useForm(); const fieldIdentifier = Form.useWatch("identifier", { form: formInst, preserve: true }); return ( ); }; export default BizApplyNodeConfigDrawer; ================================================ FILE: ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProvider.tsx ================================================ import { useEffect, useState } from "react"; import { type ACMEDns01ProviderType, type ACMEHttp01ProviderType, ACME_DNS01_PROVIDERS, ACME_HTTP01_PROVIDERS } from "@/domain/provider"; import BizApplyNodeConfigFieldsProviderAliyunESA from "./BizApplyNodeConfigFieldsProviderAliyunESA"; import BizApplyNodeConfigFieldsProviderAWSRoute53 from "./BizApplyNodeConfigFieldsProviderAWSRoute53"; import BizApplyNodeConfigFieldsProviderHuaweiCloudDNS from "./BizApplyNodeConfigFieldsProviderHuaweiCloudDNS"; import BizApplyNodeConfigFieldsProviderJDCloudDNS from "./BizApplyNodeConfigFieldsProviderJDCloudDNS"; import BizApplyNodeConfigFieldsProviderLocal from "./BizApplyNodeConfigFieldsProviderLocal"; import BizApplyNodeConfigFieldsProviderS3 from "./BizApplyNodeConfigFieldsProviderS3"; import BizApplyNodeConfigFieldsProviderSSH from "./BizApplyNodeConfigFieldsProviderSSH"; const acmeDns01ProviderComponentMap: Partial>> = { /* 注意:如果追加新的子组件,请保持以 ASCII 排序。 NOTICE: If you add new child component, please keep ASCII order. */ [ACME_DNS01_PROVIDERS.ALIYUN_ESA]: BizApplyNodeConfigFieldsProviderAliyunESA, [ACME_DNS01_PROVIDERS.AWS]: BizApplyNodeConfigFieldsProviderAWSRoute53, [ACME_DNS01_PROVIDERS.AWS_ROUTE53]: BizApplyNodeConfigFieldsProviderAWSRoute53, [ACME_DNS01_PROVIDERS.HUAWEICLOUD]: BizApplyNodeConfigFieldsProviderHuaweiCloudDNS, [ACME_DNS01_PROVIDERS.HUAWEICLOUD_DNS]: BizApplyNodeConfigFieldsProviderHuaweiCloudDNS, [ACME_DNS01_PROVIDERS.JDCLOUD]: BizApplyNodeConfigFieldsProviderJDCloudDNS, [ACME_DNS01_PROVIDERS.JDCLOUD_DNS]: BizApplyNodeConfigFieldsProviderJDCloudDNS, }; const acmeHttp01ProviderComponentMap: Partial>> = { /* 注意:如果追加新的子组件,请保持以 ASCII 排序。 NOTICE: If you add new child component, please keep ASCII order. */ [ACME_HTTP01_PROVIDERS.LOCAL]: BizApplyNodeConfigFieldsProviderLocal, [ACME_HTTP01_PROVIDERS.S3]: BizApplyNodeConfigFieldsProviderS3, [ACME_HTTP01_PROVIDERS.SSH]: BizApplyNodeConfigFieldsProviderSSH, }; const useComponent = ( challenge: "dns-01" | "http-01", provider: string, { initProps, deps = [] }: { initProps?: (provider: string) => any; deps?: unknown[] } ) => { const initComponent = () => { const Component = challenge === "dns-01" ? acmeDns01ProviderComponentMap[provider as ACMEDns01ProviderType] : challenge === "http-01" ? acmeHttp01ProviderComponentMap[provider as ACMEHttp01ProviderType] : void 0; if (!Component) return null; const props = initProps?.(provider); if (props) { return ; } return ; }; const [component, setComponent] = useState(() => initComponent()); useEffect(() => setComponent(initComponent()), [challenge, provider]); useEffect(() => setComponent(initComponent()), deps); return component; }; const _default = { useComponent, }; export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProviderAWSRoute53.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizApplyNodeConfigFieldsProviderAWSRoute53 = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { region: "us-east-1", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ region: z.string().nonempty(t("workflow_node.apply.form.aws_route53_region.placeholder")), hostedZoneId: z.string().nullish(), }); }; const _default = Object.assign(BizApplyNodeConfigFieldsProviderAWSRoute53, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProviderAliyunESA.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizApplyNodeConfigFieldsProviderAliyunESA = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { region: "cn-hangzhou", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ region: z.string().nonempty(t("workflow_node.apply.form.aliyun_esa_region.placeholder")), }); }; const _default = Object.assign(BizApplyNodeConfigFieldsProviderAliyunESA, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProviderHuaweiCloudDNS.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizApplyNodeConfigFieldsProviderHuaweiCloudDNS = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { region: "cn-north-1", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ region: z.string().nonempty(t("workflow_node.apply.form.huaweicloud_dns_region.placeholder")), }); }; const _default = Object.assign(BizApplyNodeConfigFieldsProviderHuaweiCloudDNS, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProviderJDCloudDNS.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizApplyNodeConfigFieldsProviderJDCloudDNS = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { regionId: "cn-north-1", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ regionId: z.string().nonempty(t("workflow_node.apply.form.jdcloud_dns_region_id.placeholder")), }); }; const _default = Object.assign(BizApplyNodeConfigFieldsProviderJDCloudDNS, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProviderLocal.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizApplyNodeConfigFieldsProviderLocal = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { webRootPath: "/var/www/html/", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ webRootPath: z .string() .nonempty(t("workflow_node.apply.form.local_webroot_path.placeholder")) .refine((v) => !!v && (v.endsWith("/") || v.endsWith("\\")), { error: t("workflow_node.apply.form.local_webroot_path.placeholder"), }), }); }; const _default = Object.assign(BizApplyNodeConfigFieldsProviderLocal, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProviderS3.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizApplyNodeConfigFieldsProviderS3 = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> ); }; const getInitialValues = (): Nullish>> => { return { region: "", bucket: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ region: z.string().nonempty(t("workflow_node.apply.form.s3_region.placeholder")), bucket: z.string().nonempty(t("workflow_node.apply.form.s3_bucket.placeholder")), }); }; const _default = Object.assign(BizApplyNodeConfigFieldsProviderS3, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProviderSSH.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Switch } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizApplyNodeConfigFieldsProviderSSH = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { webRootPath: "/var/www/html/", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ webRootPath: z .string() .nonempty(t("workflow_node.apply.form.ssh_webroot_path.placeholder")) .refine((v) => !!v && (v.endsWith("/") || v.endsWith("\\")), { error: t("workflow_node.apply.form.ssh_webroot_path.placeholder"), }), useSCP: z.boolean().nullish(), }); }; const _default = Object.assign(BizApplyNodeConfigFieldsProviderSSH, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx ================================================ import { memo, useEffect, useMemo, useState } from "react"; import { getI18n, useTranslation } from "react-i18next"; import { Link } from "react-router"; import { type FlowNodeEntity } from "@flowgram.ai/fixed-layout-editor"; import { IconArrowRight, IconChevronRight, IconCircleMinus, IconMapPin, IconPlus, IconWorldWww } from "@tabler/icons-react"; import { useControllableValue, useMount } from "ahooks"; import { type AnchorProps, AutoComplete, Avatar, Button, Card, Divider, Form, type FormInstance, Input, InputNumber, Radio, Select, Space, Switch, Typography, } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import AccessEditDrawer from "@/components/access/AccessEditDrawer"; import AccessSelect from "@/components/access/AccessSelect"; import FileTextInput from "@/components/FileTextInput"; import MultipleSplitValueInput from "@/components/MultipleSplitValueInput"; import ACMEDns01ProviderSelect from "@/components/provider/ACMEDns01ProviderSelect"; import ACMEHttp01ProviderSelect from "@/components/provider/ACMEHttp01ProviderSelect"; import CAProviderSelect from "@/components/provider/CAProviderSelect"; import Show from "@/components/Show"; import { type AccessModel } from "@/domain/access"; import { CA_PROVIDERS, acmeDns01ProvidersMap, acmeHttp01ProvidersMap, caProvidersMap } from "@/domain/provider"; import { type WorkflowNodeConfigForBizApply, defaultNodeConfigForBizApply } from "@/domain/workflow"; import { useAntdForm, useZustandShallowSelector } from "@/hooks"; import { useAccessesStore } from "@/stores/access"; import { useContactEmailsStore } from "@/stores/settings"; import { mergeCls } from "@/utils/css"; import { matchSearchOption } from "@/utils/search"; import { isDomain, isHostname, isIPv4, isIPv6 } from "@/utils/validator"; import { getPrivateKeyAlgorithm as getPKIXPrivateKeyAlgorithm, validatePEMPrivateKey } from "@/utils/x509"; import { FormNestedFieldsContextProvider, NodeFormContextProvider } from "./_context"; import BizApplyNodeConfigFieldsProvider from "./BizApplyNodeConfigFieldsProvider"; import { NodeType } from "../nodes/typings"; const MULTIPLE_INPUT_SEPARATOR = ";"; const IDENTIFIER_DOMAIN = "domain" as const; const IDENTIFIER_IP = "ip" as const; const CHALLENGE_TYPE_DNS01 = "dns-01" as const; const CHALLENGE_TYPE_HTTP01 = "http-01" as const; const KEY_SOURCE_AUTO = "auto" as const; const KEY_SOURCE_REUSE = "reuse" as const; const KEY_SOURCE_CUSTOM = "custom" as const; export interface BizApplyNodeConfigFormProps { form: FormInstance; node: FlowNodeEntity; } const BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps) => { if (node.flowNodeType !== NodeType.BizApply) { console.warn(`[certimate] current workflow node type is not: ${NodeType.BizApply}`); } const { i18n, t } = useTranslation(); const { accesses } = useAccessesStore(useZustandShallowSelector("accesses")); const accessOptionFilter = (_: string, option: AccessModel) => { if (option.reserve) return false; if (fieldChallengeType === CHALLENGE_TYPE_DNS01) return acmeDns01ProvidersMap.get(fieldProvider)?.provider === option.provider; if (fieldChallengeType === CHALLENGE_TYPE_HTTP01) return acmeHttp01ProvidersMap.get(fieldProvider)?.provider === option.provider; return false; }; const accessOptionFilterForCA = (_: string, option: AccessModel) => { if (option.reserve !== "ca") return false; return caProvidersMap.get(fieldCAProvider!)?.provider === option.provider; }; const initialValues = useMemo(() => { return node.form?.getValueIn("config") as WorkflowNodeConfigForBizApply | undefined; }, [node]); const formSchema = getSchema({ i18n }); type FormSchema = z.infer; const formRule = createSchemaFieldRule(formSchema); const { form: formInst, formProps } = useAntdForm({ form: props.form, name: "workflowNodeBizApplyConfigForm", initialValues: initialValues ?? getInitialValues(), }); const fieldIdentifier = Form.useWatch("identifier", { form: formInst, preserve: true }); const fieldChallengeType = Form.useWatch("challengeType", { form: formInst, preserve: true }); const fieldProvider = Form.useWatch("provider", { form: formInst, preserve: true }); const fieldProviderAccessId = Form.useWatch("providerAccessId", { form: formInst, preserve: true }); const fieldKeySource = Form.useWatch("keySource", { form: formInst, preserve: true }); const fieldCAProvider = Form.useWatch("caProvider", { form: formInst, preserve: true }); const fieldCAProviderAccessId = Form.useWatch("caProviderAccessId", { form: formInst, preserve: true }); const renderNestedFieldProviderComponent = BizApplyNodeConfigFieldsProvider.useComponent(fieldChallengeType, fieldProvider, {}); const resetFieldIfInvalid = (field: keyof FormSchema) => { const fieldSchema = formSchema.pick({ [field]: true } as Record); const fieldValue = formInst.getFieldValue(field); if (!fieldSchema.safeParse({ [field]: fieldValue }).success) { formInst.setFieldValue(field, void 0); } }; const showProviderAccess = useMemo(() => { // 内置的质询提供商(如本地主机)无需显示授权信息字段 switch (fieldChallengeType) { case CHALLENGE_TYPE_DNS01: { if (fieldProvider) { const provider = acmeDns01ProvidersMap.get(fieldProvider); return !provider?.builtin; } } break; case CHALLENGE_TYPE_HTTP01: { if (fieldProvider) { const provider = acmeHttp01ProvidersMap.get(fieldProvider); return !provider?.builtin; } } break; } return false; }, [fieldChallengeType, fieldProvider]); const showCAProviderAccess = useMemo(() => { // 内置的 CA 提供商(如 Let's Encrypt)无需显示授权信息字段 if (fieldCAProvider) { const provider = caProvidersMap.get(fieldCAProvider); return !provider?.builtin; } return false; }, [fieldCAProvider]); useEffect(() => { // 如果未选择质询提供商,则清空授权信息 if (!fieldProvider && fieldProviderAccessId) { formInst.setFieldValue("providerAccessId", void 0); return; } // 如果已选择质询提供商只有一个授权信息,则自动选择该授权信息 if (fieldProvider && !fieldProviderAccessId) { const availableAccesses = accesses .filter((access) => accessOptionFilter(access.provider, access)) .filter((access) => { if (fieldChallengeType === CHALLENGE_TYPE_DNS01) return acmeDns01ProvidersMap.get(fieldProvider)?.provider === access.provider; if (fieldChallengeType === CHALLENGE_TYPE_HTTP01) return acmeHttp01ProvidersMap.get(fieldProvider)?.provider === access.provider; return false; }); if (availableAccesses.length === 1) { formInst.setFieldValue("providerAccessId", availableAccesses[0].id); } } }, [fieldChallengeType, fieldProvider, fieldProviderAccessId]); useEffect(() => { // 如果未选择 CA 提供商,则清空授权信息 if (!fieldCAProvider && fieldCAProviderAccessId) { formInst.setFieldValue("caProviderAccessId", void 0); return; } // 如果已选择 CA 提供商只有一个授权信息,则自动选择该授权信息 if (fieldCAProvider && !fieldCAProviderAccessId) { const availableAccesses = accesses .filter((access) => accessOptionFilterForCA(access.provider, access)) .filter((access) => caProvidersMap.get(fieldCAProvider)?.provider === access.provider); if (availableAccesses.length === 1) { formInst.setFieldValue("caProviderAccessId", availableAccesses[0].id); } } }, [fieldCAProvider, fieldCAProviderAccessId]); const handleIdentifierPick = (value: string) => { switch (value) { case IDENTIFIER_DOMAIN: { formInst.setFieldValue("identifier", IDENTIFIER_DOMAIN); formInst.setFieldValue("domains", formInst.getFieldValue("domains") || ""); formInst.setFieldValue("challengeType", CHALLENGE_TYPE_DNS01); } break; case IDENTIFIER_IP: { formInst.setFieldValue("identifier", IDENTIFIER_IP); formInst.setFieldValue("ipaddrs", formInst.getFieldValue("ipaddrs") || ""); formInst.setFieldValue("challengeType", CHALLENGE_TYPE_HTTP01); formInst.setFieldValue("caProvider", CA_PROVIDERS.LETSENCRYPT); formInst.setFieldValue("caProviderAccessId", void 0); formInst.setFieldValue("caProviderConfig", void 0); formInst.setFieldValue("acmeProfile", "shortlived"); formInst.setFieldValue("disableCommonName", true); formInst.setFieldValue("skipBeforeExpiryDays", 3); } break; } setTimeout(() => handleIdentifierChange(value), 0); }; const handleIdentifierChange = (value: string) => { switch (value) { case IDENTIFIER_DOMAIN: { formInst.setFieldValue("ipaddrs", void 0); } break; case IDENTIFIER_IP: { formInst.setFieldValue("domains", void 0); resetFieldIfInvalid("nameservers"); } break; } }; const handleChallengeTypeChange = (value: string) => { switch (value) { case CHALLENGE_TYPE_DNS01: { formInst.setFieldValue("provider", void 0); formInst.setFieldValue("providerAccessId", void 0); formInst.setFieldValue("providerConfig", void 0); resetFieldIfInvalid("httpDelayWait"); } break; case CHALLENGE_TYPE_HTTP01: { formInst.setFieldValue("provider", void 0); formInst.setFieldValue("providerAccessId", void 0); formInst.setFieldValue("providerConfig", void 0); resetFieldIfInvalid("dnsPropagationWait"); resetFieldIfInvalid("dnsPropagationTimeout"); resetFieldIfInvalid("dnsTTL"); } break; } }; const handleProviderSelect = (value?: string | undefined) => { // 切换质询提供商时重置表单,避免其他提供商的配置字段影响当前提供商 if (initialValues?.provider === value) { formInst.setFieldValue("providerAccessId", void 0); formInst.resetFields(["providerConfig"]); } else { formInst.setFieldValue("providerAccessId", void 0); formInst.setFieldValue("providerConfig", void 0); } }; const handleKeySourceChange = (value: string) => { if (value === initialValues?.keySource) { formInst.resetFields(["keyContent"]); } else { setTimeout(() => { formInst.setFieldValue("keyContent", ""); }, 0); } }; const handleCAProviderSelect = (value?: string | undefined) => { // 切换 CA 提供商时联动授权信息 if (value == null || value === "") { formInst.setFieldValue("caProvider", void 0); formInst.setFieldValue("caProviderAccessId", void 0); } else if (value === initialValues?.caProvider) { formInst.setFieldValue("caProviderAccessId", initialValues?.caProviderAccessId); } else { if (caProvidersMap.get(fieldCAProvider!)?.provider !== caProvidersMap.get(value!)?.provider) { formInst.setFieldValue("caProviderAccessId", void 0); } } }; return (
} rules={[formRule]} > } rules={[formRule]} > } >
{t("workflow_node.apply.form_anchor.challenge.title")} } > handleChallengeTypeChange(e.target.value)}> DNS-01 HTTP-01 {fieldChallengeType === CHALLENGE_TYPE_DNS01 ? ( ) : fieldChallengeType === CHALLENGE_TYPE_HTTP01 ? ( ) : ( ({ label: e, value: e, }))} placeholder={t("workflow_node.apply.form.key_algorithm.placeholder")} />
} > } > ({ label: e.ca, options: e.roots.map((s) => ({ label: s, value: s, })), }))} placeholder={t("workflow_node.apply.form.preferred_chain.placeholder")} showSearch={{ filterOption: (inputValue, option) => matchSearchOption(inputValue, option!), }} /> } > ({ label: e.ca, options: e.profiles.map((s) => ({ label: s, value: s, })), }))} placeholder={t("workflow_node.apply.form.acme_profile.placeholder")} showSearch={{ filterOption: (inputValue, option) => matchSearchOption(inputValue, option!), }} /> } >
{t("workflow_node.apply.form_anchor.advanced.title")} } >
{t("workflow_node.apply.form_anchor.strategy.title")} {t("workflow_node.apply.form.skip_before_expiry_days.prefix")} {t("workflow_node.apply.form.skip_before_expiry_days.suffix")}
); }; const InternalIdentifierPicker = memo(({ disabled, onSelect }: { disabled?: boolean; onSelect?: (value: string) => void }) => { const { t } = useTranslation(); const [value, setValue] = useState(); const options = [ { value: IDENTIFIER_DOMAIN, label: t("workflow_node.apply.form.identifier.option.domain.label"), description: t("workflow_node.apply.form.identifier.option.domain.description"), icon: , }, { value: IDENTIFIER_IP, label: t("workflow_node.apply.form.identifier.option.ip.label"), description: t("workflow_node.apply.form.identifier.option.ip.description"), icon: , }, ]; const handleContinueClick = () => { if (!value) return; onSelect?.(value); }; return ( <>
{options.map((option) => ( { if (disabled) return; setValue(option.value); }} >
{option.label}
{value === option.value &&
}
))}
); }); const InternalEmailInput = memo(({ disabled, ...props }: { disabled?: boolean; value?: string; onChange?: (value: string) => void }) => { const { t } = useTranslation(); const { emails, fetchEmails, removeEmail } = useContactEmailsStore(); useMount(() => { fetchEmails(false); }); const [value, setValue] = useControllableValue(props, { valuePropName: "value", defaultValuePropName: "defaultValue", trigger: "onChange", }); const [inputValue, setInputValue] = useState(); const renderOptionLabel = (email: string, removable: boolean = false) => (
{email} {removable && (
); const options = useMemo(() => { const temp = emails.map((email) => ({ label: renderOptionLabel(email, true), value: email, })); if (!!inputValue && temp.every((option) => option.value !== inputValue)) { temp.unshift({ label: renderOptionLabel(inputValue), value: inputValue, }); } return temp; }, [emails, inputValue]); const handleChange = (value: string) => { setValue(value); }; const handleSearch = (value: string) => { setInputValue(value?.trim()); }; return ( ); }); const InternalValidityLifetimeInput = memo(({ disabled, ...props }: { disabled?: boolean; value?: string; onChange?: (value: string) => void }) => { const { t } = useTranslation(); const [value, setValue] = useControllableValue(props, { valuePropName: "value", defaultValuePropName: "defaultValue", trigger: "onChange", }); const parseCombinedValue = (val: string): [string | undefined, string | undefined] => { const match = String(val).match(/^(\d+)([a-zA-Z]+)$/); if (match) { return [match[1], match[2]]; } return [undefined, undefined]; }; const [inputValue, setInputValue] = useState(parseCombinedValue(value)[0]); const [selectValue, setSelectValue] = useState(parseCombinedValue(value)[1] || "d"); useEffect(() => { const [v, u] = parseCombinedValue(value); setInputValue(v); setSelectValue(u || "d"); }, [value]); const handleInputClear = () => { setValue(""); }; const handleInputChange = (e: React.ChangeEvent) => { setInputValue(e.currentTarget.value); if (e.currentTarget.value) { setValue(`${e.currentTarget.value}${selectValue}`); } else { setValue(""); } }; const handleSelectChange = (value: string) => { setSelectValue(value); if (inputValue) { setValue(`${inputValue}${value}`); } }; return (
} > ); }; const getInitialValues = (): Nullish>> => { return { resourceType: RESOURCE_TYPE_WEBSITE, websiteMatchPattern: WEBSITE_MATCH_PATTERN_SPECIFIED, websiteId: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ nodeName: z.string().nullish(), resourceType: z.literal([RESOURCE_TYPE_WEBSITE, RESOURCE_TYPE_CERTIFICATE], t("workflow_node.deploy.form.shared_resource_type.placeholder")), websiteMatchPattern: z.string().nullish(), websiteId: z.union([z.string(), z.number().int()]).nullish(), certificateId: z.union([z.string(), z.number().int()]).nullish(), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_WEBSITE: { if (values.websiteMatchPattern) { switch (values.websiteMatchPattern) { case WEBSITE_MATCH_PATTERN_SPECIFIED: { const scWebsiteId = z.coerce.number().int().positive(); if (!scWebsiteId.safeParse(values.websiteId).success) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.1panel_website_id.placeholder"), path: ["websiteId"], }); } } break; } } else { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.1panel_website_match_pattern.placeholder"), path: ["websiteMatchPattern"], }); } } break; case RESOURCE_TYPE_CERTIFICATE: { const scCertificateId = z.coerce.number().int().positive(); if (!scCertificateId.safeParse(values.certificateId).success) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.1panel_certificate_id.placeholder"), path: ["websiteId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProvider1Panel, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider1PanelConsole.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Switch } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProvider1PanelConsole = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> ); }; const getInitialValues = (): Nullish>> => { return { autoRestart: true, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t: _ } = i18n; return z.object({ autoRestart: z.boolean().nullish(), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProvider1PanelConsole, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAPISIX.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import Tips from "@/components/Tips"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; const BizDeployNodeConfigFieldsProviderAPISIX = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> } /> ); }; const getInitialValues = (): Nullish>> => { return { resourceType: RESOURCE_TYPE_CERTIFICATE, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, t("workflow_node.deploy.form.shared_resource_type.placeholder")), certificateId: z.string().nullish(), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_CERTIFICATE: { if (!values.certificateId?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.apisix_certificate_id.placeholder"), path: ["certificateId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderAPISIX, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAWSACM.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderAWSACM = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ region: z.string().nonempty(t("workflow_node.deploy.form.aws_acm_region.placeholder")), certificateArn: z.string().nullish(), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderAWSACM, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAWSCloudFront.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderAWSCloudFront = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { certificateSource: "ACM", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ region: z.string().nonempty(t("workflow_node.deploy.form.aws_cloudfront_region.placeholder")), distributionId: z.string().nonempty(t("workflow_node.deploy.form.aws_cloudfront_distribution_id.placeholder")), certificateSource: z.string().nonempty(t("workflow_node.deploy.form.aws_cloudfront_certificate_source.placeholder")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderAWSCloudFront, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAWSIAM.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderAWSIAM = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", certificatePath: "/", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ region: z.string().nonempty(t("workflow_node.deploy.form.aws_iam_region.placeholder")), certificatePath: z .string() .nullish() .refine((v) => { if (!v) return true; return v.startsWith("/") && v.endsWith("/"); }, t("workflow_node.deploy.form.aws_iam_certificate_path.errmsg.invalid")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderAWSIAM, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunALB.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; const RESOURCE_TYPE_LISTENER = "listener" as const; const BizDeployNodeConfigFieldsProviderAliyunALB = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", resourceType: RESOURCE_TYPE_LISTENER, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ region: z.string().nonempty(t("workflow_node.deploy.form.aliyun_alb_region.placeholder")), resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t("workflow_node.deploy.form.shared_resource_type.placeholder")), loadbalancerId: z.string().nullish(), listenerId: z.string().nullish(), domain: z .string() .nullish() .refine((v) => { if (!v) return true; return isDomain(v, { allowWildcard: true }); }, t("common.errmsg.domain_invalid")), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_LOADBALANCER: { const scLoadbalancerId = z.string().nonempty(); if (!scLoadbalancerId.safeParse(values.loadbalancerId).success) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.aliyun_alb_loadbalancer_id.placeholder"), path: ["loadbalancerId"], }); } } break; case RESOURCE_TYPE_LISTENER: { const scListenerId = z.string().nonempty(); if (!scListenerId.safeParse(values.listenerId).success) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.aliyun_alb_listener_id.placeholder"), path: ["listenerId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunALB, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunAPIGW.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const SERVICE_TYPE_CLOUDNATIVE = "cloudnative" as const; const SERVICE_TYPE_TRADITIONAL = "traditional" as const; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderAliyunAPIGW = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldServiceType = Form.useWatch([parentNamePath, "serviceType"], formInst); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> } > } > ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { region: "", domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ serviceType: z.literal([SERVICE_TYPE_CLOUDNATIVE, SERVICE_TYPE_TRADITIONAL], t("workflow_node.deploy.form.aliyun_apigw_service_type.placeholder")), region: z.string().nonempty(t("workflow_node.deploy.form.aliyun_apigw_region.placeholder")), gatewayId: z.string().nullish(), groupId: z.string().nullish(), domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.serviceType) { switch (values.serviceType) { case SERVICE_TYPE_CLOUDNATIVE: { if (!values.gatewayId?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.aliyun_apigw_gateway_id.placeholder"), path: ["gatewayId"], }); } } break; case SERVICE_TYPE_TRADITIONAL: { if (!values.groupId?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.aliyun_apigw_group_id.placeholder"), path: ["groupId"], }); } } break; } } if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunAPIGW, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunCAS.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderAliyunCAS = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ region: z.string().nonempty(t("workflow_node.deploy.form.aliyun_cas_region.placeholder")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunCAS, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunCASDeploy.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import MultipleSplitValueInput from "@/components/MultipleSplitValueInput"; import Tips from "@/components/Tips"; import { useFormNestedFieldsContext } from "./_context"; const MULTIPLE_INPUT_SEPARATOR = ";"; const BizDeployNodeConfigFieldsProviderAliyunCASDeploy = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } /> } > } > } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", resourceIds: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ region: z.string().nonempty(t("workflow_node.deploy.form.aliyun_casdeploy_region.placeholder")), resourceIds: z.string().refine((v) => { if (!v) return false; return v.split(MULTIPLE_INPUT_SEPARATOR).every((e) => /^[1-9]\d*$/.test(e)); }, t("workflow_node.deploy.form.aliyun_casdeploy_resource_ids.errmsg.invalid")), contactIds: z .string() .nullish() .refine((v) => { if (!v) return true; return v.split(MULTIPLE_INPUT_SEPARATOR).every((e) => /^[1-9]\d*$/.test(e)); }, t("workflow_node.deploy.form.aliyun_casdeploy_contact_ids.errmsg.invalid")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunCASDeploy, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunCDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { AutoComplete, Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderAliyunCDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> } > ({ value: s }))} placeholder={t("workflow_node.deploy.form.aliyun_cdn_region.placeholder")} /> ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { region: "", domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ region: z.string().nullish(), domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunCDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunCLB.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain, isPortNumber } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; const RESOURCE_TYPE_LISTENER = "listener" as const; const BizDeployNodeConfigFieldsProviderAliyunCLB = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", resourceType: RESOURCE_TYPE_LISTENER, loadbalancerId: "", listenerPort: 443, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ region: z.string().nonempty(t("workflow_node.deploy.form.aliyun_clb_region.placeholder")), resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t("workflow_node.deploy.form.shared_resource_type.placeholder")), loadbalancerId: z.string().nonempty(t("workflow_node.deploy.form.aliyun_clb_loadbalancer_id.placeholder")), listenerPort: z.preprocess((v) => (v == null || v === "" ? void 0 : Number(v)), z.number().nullish()), domain: z .string() .nullish() .refine((v) => { if (!v) return true; return isDomain(v, { allowWildcard: true }); }, t("common.errmsg.domain_invalid")), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_LISTENER: { if (!isPortNumber(values.listenerPort!)) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.aliyun_clb_listener_port.placeholder"), path: ["listenerPort"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunCLB, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunDCDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { AutoComplete, Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderAliyunDCDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> } > ({ value: s }))} placeholder={t("workflow_node.deploy.form.aliyun_dcdn_region.placeholder")} /> ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { region: "", domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ region: z.string().nullish(), domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunDCDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunDDoSPro.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderAliyunDDoSPro = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> } > ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { region: "", domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ region: z.string().nonempty(t("workflow_node.deploy.form.aliyun_ddospro_region.placeholder")), domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunDDoSPro, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunESA.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderAliyunESA = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", siteId: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ region: z.string().nonempty(t("workflow_node.deploy.form.aliyun_esa_region.placeholder")), siteId: z.union([z.string(), z.number().int()]).refine((v) => { return /^\d+$/.test(v + "") && +v > 0; }, t("workflow_node.deploy.form.aliyun_esa_site_id.placeholder")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunESA, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunESASaaS.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderAliyunESASaaS = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> } > } > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { region: "", siteId: "", domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ region: z.string().nonempty(t("workflow_node.deploy.form.aliyun_esa_saas_region.placeholder")), siteId: z.union([z.string(), z.number().int()]).refine((v) => { return /^\d+$/.test(v + "") && +v > 0; }, t("workflow_node.deploy.form.aliyun_esa_saas_site_id.placeholder")), domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunESASaaS, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunFC.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderAliyunFC = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> } > ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { region: "", serviceVersion: "3.0", domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ region: z.string().nonempty(t("workflow_node.deploy.form.aliyun_fc_region.placeholder")), serviceVersion: z.literal(["2.0", "3.0"], t("workflow_node.deploy.form.aliyun_fc_service_version.placeholder")), domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunFC, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunGA.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_ACCELERATOR = "accelerator" as const; const RESOURCE_TYPE_LISTENER = "listener" as const; const BizDeployNodeConfigFieldsProviderAliyunGA = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { resourceType: RESOURCE_TYPE_LISTENER, acceleratorId: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ resourceType: z.literal([RESOURCE_TYPE_ACCELERATOR, RESOURCE_TYPE_LISTENER], t("workflow_node.deploy.form.shared_resource_type.placeholder")), acceleratorId: z.string().nonempty(t("workflow_node.deploy.form.aliyun_ga_accelerator_id.placeholder")), listenerId: z.string().nullish(), domain: z .string() .nullish() .refine((v) => { if (!v) return true; return isDomain(v); }, t("common.errmsg.domain_invalid")), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_LISTENER: { if (!values.listenerId?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.aliyun_ga_listener_id.placeholder"), path: ["listenerId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunGA, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunLive.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderAliyunLive = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> } > ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { region: "", domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ region: z.string().nonempty(t("workflow_node.deploy.form.aliyun_live_region.placeholder")), domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunLive, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunNLB.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; const RESOURCE_TYPE_LISTENER = "listener" as const; const BizDeployNodeConfigFieldsProviderAliyunNLB = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", resourceType: RESOURCE_TYPE_LISTENER, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ region: z.string().nonempty(t("workflow_node.deploy.form.aliyun_nlb_region.placeholder")), resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t("workflow_node.deploy.form.shared_resource_type.placeholder")), loadbalancerId: z.string().nullish(), listenerId: z.string().nullish(), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_LOADBALANCER: { if (!values.loadbalancerId?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.aliyun_nlb_loadbalancer_id.placeholder"), path: ["loadbalancerId"], }); } } break; case RESOURCE_TYPE_LISTENER: { if (!values.listenerId?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.aliyun_nlb_listener_id.placeholder"), path: ["listenerId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunNLB, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunOSS.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderAliyunOSS = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", bucket: "", domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ region: z.string().nonempty(t("workflow_node.deploy.form.aliyun_oss_region.placeholder")), bucket: z.string().nonempty(t("workflow_node.deploy.form.aliyun_oss_bucket.placeholder")), domain: z.string().refine((v) => isDomain(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunOSS, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunVOD.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderAliyunVOD = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> } > ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { region: "", domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ region: z.string().nonempty(t("workflow_node.deploy.form.aliyun_vod_region.placeholder")), domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunVOD, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunWAF.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { AutoComplete, Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { matchSearchOption } from "@/utils/search"; import { isDomain, isPortNumber } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const SERVICE_TYPE_CLOUDRESOURCE = "cloudresource" as const; const SERVICE_TYPE_CNAME = "cname" as const; const BizDeployNodeConfigFieldsProviderAliyunWAF = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldServiceType = Form.useWatch([parentNamePath, "serviceType"], formInst); return ( <> } > ({ value }))} placeholder={t("workflow_node.deploy.form.aliyun_waf_resource_product.placeholder")} showSearch={{ filterOption: (inputValue, option) => matchSearchOption(inputValue, option!), }} /> ); }; const getInitialValues = (): Nullish>> => { return { region: "", serviceVersion: "3.0", instanceId: "", resourceProduct: "", resourceId: "", resourcePort: 443, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ region: z.string().nonempty(t("workflow_node.deploy.form.aliyun_waf_region.placeholder")), serviceVersion: z.literal("3.0", t("workflow_node.deploy.form.aliyun_waf_service_version.placeholder")), serviceType: z.literal([SERVICE_TYPE_CLOUDRESOURCE, SERVICE_TYPE_CNAME], t("workflow_node.deploy.form.aliyun_waf_service_type.placeholder")), instanceId: z.string().nonempty(t("workflow_node.deploy.form.aliyun_waf_instance_id.placeholder")), resourceProduct: z.string().nullish(), resourceId: z.string().nullish(), resourcePort: z.preprocess((v) => (v == null || v === "" ? void 0 : Number(v)), z.number().nullish()), domain: z .string() .nullish() .refine((v) => { if (!v) return true; return isDomain(v, { allowWildcard: true }); }, t("common.errmsg.domain_invalid")), }) .superRefine((values, ctx) => { switch (values.serviceType) { case SERVICE_TYPE_CLOUDRESOURCE: { if (!values.resourceProduct) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.aliyun_waf_resource_product.placeholder"), path: ["resourceProduct"], }); } if (!values.resourceId) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.aliyun_waf_resource_id.placeholder"), path: ["resourceId"], }); } if (!isPortNumber(values.resourcePort!)) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.aliyun_waf_resource_port.placeholder"), path: ["resourcePort"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderAliyunWAF, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAzureKeyVault.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderAzureKeyVault = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { keyvaultName: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ keyvaultName: z.string().nonempty(t("workflow_node.deploy.form.azure_keyvault_name.placeholder")), certificateName: z .string() .nullish() .refine((v) => { if (!v) return true; return /^[a-zA-Z0-9-]{1,127}$/.test(v); }, t("workflow_node.deploy.form.azure_keyvault_certificate_name.errmsg.invalid")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderAzureKeyVault, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderBaiduCloudAppBLB.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain, isPortNumber } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; const RESOURCE_TYPE_LISTENER = "listener" as const; const BizDeployNodeConfigFieldsProviderBaiduCloudAppBLB = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", resourceType: RESOURCE_TYPE_LISTENER, loadbalancerId: "", listenerPort: 443, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ region: z.string().nonempty(t("workflow_node.deploy.form.baiducloud_appblb_region.placeholder")), resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t("workflow_node.deploy.form.shared_resource_type.placeholder")), loadbalancerId: z.string().nonempty(t("workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.placeholder")), listenerPort: z.preprocess((v) => (v == null || v === "" ? void 0 : Number(v)), z.number().nullish()), domain: z .string() .nullish() .refine((v) => { if (!v) return true; return isDomain(v, { allowWildcard: true }); }, t("common.errmsg.domain_invalid")), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_LISTENER: { if (!isPortNumber(values.listenerPort!)) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.baiducloud_appblb_listener_port.placeholder"), path: ["listenerPort"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderBaiduCloudAppBLB, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderBaiduCloudBLB.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain, isPortNumber } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; const RESOURCE_TYPE_LISTENER = "listener" as const; const BizDeployNodeConfigFieldsProviderBaiduCloudBLB = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", resourceType: RESOURCE_TYPE_LISTENER, loadbalancerId: "", listenerPort: 443, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ region: z.string().nonempty(t("workflow_node.deploy.form.baiducloud_blb_region.placeholder")), resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t("workflow_node.deploy.form.shared_resource_type.placeholder")), loadbalancerId: z.string().nonempty(t("workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.placeholder")), listenerPort: z.preprocess((v) => (v == null || v === "" ? void 0 : Number(v)), z.number().nullish()), domain: z .string() .nullish() .refine((v) => { if (!v) return true; return isDomain(v, { allowWildcard: true }); }, t("common.errmsg.domain_invalid")), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_LISTENER: { if (!isPortNumber(values.listenerPort!)) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.baiducloud_blb_listener_port.placeholder"), path: ["listenerPort"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderBaiduCloudBLB, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderBaiduCloudCDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderBaiduCloudCDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderBaiduCloudCDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderBaishanCDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_DOMAIN = "domain" as const; const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const BizDeployNodeConfigFieldsProviderBaishanCDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { resourceType: RESOURCE_TYPE_DOMAIN, domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ resourceType: z.literal([RESOURCE_TYPE_DOMAIN, RESOURCE_TYPE_CERTIFICATE], t("workflow_node.deploy.form.shared_resource_type.placeholder")), domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), certificateId: z.union([z.string(), z.number().int()]).nullish(), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_DOMAIN: { if (!values.domainMatchPattern) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder"), path: ["domainMatchPattern"], }); } switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } break; case RESOURCE_TYPE_CERTIFICATE: { if (!values.certificateId) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.baishan_cdn_certificate_id.placeholder"), path: ["certificateId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderBaishanCDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderBaotaPanel.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import MultipleSplitValueInput from "@/components/MultipleSplitValueInput"; import Tips from "@/components/Tips"; import { useFormNestedFieldsContext } from "./_context"; const MULTIPLE_INPUT_SEPARATOR = ";"; const BizDeployNodeConfigFieldsProviderBaotaPanel = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } /> ({ value: s, label: t(`workflow_node.deploy.form.baotapanelgo_site_type.option.${s}.label`), }))} placeholder={t("workflow_node.deploy.form.shared_resource_type.placeholder")} /> } > ); }; const getInitialValues = (): Nullish>> => { return { siteType: "php", siteNames: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ siteType: z.string().nonempty(t("workflow_node.deploy.form.baotapanelgo_site_type.placeholder")), siteNames: z .string() .nonempty(t("workflow_node.deploy.form.baotapanelgo_site_names.placeholder")) .refine( (v) => { if (!v) return false; return v.split(MULTIPLE_INPUT_SEPARATOR).every((s) => !!s.trim()); }, { error: t("workflow_node.deploy.form.baotapanelgo_site_names.placeholder") } ), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderBaotaPanelGo, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderBaotaPanelGoConsole.tsx ================================================ import { getI18n } from "react-i18next"; import { z } from "zod"; const BizDeployNodeConfigFieldsProviderBaotaPanelConsoleGo = () => { return <>; }; const getInitialValues = (): Nullish>> => { return {}; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t: _ } = i18n; return z.object({}); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderBaotaPanelConsoleGo, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderBaotaWAF.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, InputNumber } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import MultipleSplitValueInput from "@/components/MultipleSplitValueInput"; import Tips from "@/components/Tips"; import { isPortNumber } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const MULTIPLE_INPUT_SEPARATOR = ";"; const BizDeployNodeConfigFieldsProviderBaotaWAF = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } /> } > ); }; const getInitialValues = (): Nullish>> => { return { siteNames: "", sitePort: 443, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ siteNames: z .string() .nonempty(t("workflow_node.deploy.form.baotawaf_site_names.placeholder")) .refine( (v) => { if (!v) return false; return v.split(MULTIPLE_INPUT_SEPARATOR).every((s) => !!s.trim()); }, { error: t("workflow_node.deploy.form.baotawaf_site_names.placeholder") } ), sitePort: z.coerce.number().refine((v) => isPortNumber(v), t("common.errmsg.port_invalid")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderBaotaWAF, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderBaotaWAFConsole.tsx ================================================ import type { getI18n } from "react-i18next"; import { useTranslation } from "react-i18next"; import { Form } from "antd"; import { z } from "zod"; import Tips from "@/components/Tips"; const BizDeployNodeConfigFieldsProviderBaotaWAFConsole = () => { const { t } = useTranslation(); return ( <> } /> ); }; const getInitialValues = (): Nullish>> => { return {}; }; const getSchema = (_: { i18n?: ReturnType }) => { return z.object({}); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderBaotaWAFConsole, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderBunnyCDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderBunnyCDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { pullZoneId: "", hostname: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ pullZoneId: z.union([z.string(), z.number().int()]).refine((v) => { return /^\d+$/.test(v + "") && +v! > 0; }, t("workflow_node.deploy.form.bunny_cdn_pull_zone_id.placeholder")), hostname: z .string() .nonempty(t("workflow_node.deploy.form.bunny_cdn_hostname.placeholder")) .refine((v) => { return isDomain(v!, { allowWildcard: true }); }, t("common.errmsg.domain_invalid")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderBunnyCDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderBytePlusCDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderBytePlusCDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderBytePlusCDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderCPanel.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_WEBSITE = "website" as const; const BizDeployNodeConfigFieldsProviderCPanel = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> ); }; const getInitialValues = (): Nullish>> => { return { resourceType: RESOURCE_TYPE_WEBSITE, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ resourceType: z.literal(RESOURCE_TYPE_WEBSITE, t("workflow_node.deploy.form.cpanel_resource_type.placeholder")), domain: z.string().nullish(), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_WEBSITE: { if (!isDomain(values.domain!)) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.cpanel_domain.placeholder"), path: ["domain"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderCPanel, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderCTCCCloudAO.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderCTCCCloudAO = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderCTCCCloudAO, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderCTCCCloudCDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderCTCCCloudCDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderCTCCCloudCDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderCTCCCloudELB.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; const RESOURCE_TYPE_LISTENER = "listener" as const; const BizDeployNodeConfigFieldsProviderCTCCCloudELB = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { regionId: "", resourceType: RESOURCE_TYPE_LISTENER, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ regionId: z.string().nonempty(t("workflow_node.deploy.form.ctcccloud_elb_region_id.placeholder")), resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t("workflow_node.deploy.form.shared_resource_type.placeholder")), loadbalancerId: z.string().nullish(), listenerId: z.string().nullish(), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_LOADBALANCER: { if (!values.loadbalancerId?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.ctcccloud_elb_loadbalancer_id.placeholder"), path: ["loadbalancerId"], }); } } break; case RESOURCE_TYPE_LISTENER: { if (!values.listenerId?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.ctcccloud_elb_listener_id.placeholder"), path: ["listenerId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderCTCCCloudELB, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderCTCCCloudFaaS.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderCTCCCloudFaaS = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { regionId: "", domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ regionId: z.string().nonempty(t("workflow_node.deploy.form.ctcccloud_faas_region_id.placeholder")), domain: z.string().refine((v) => isDomain(v), t("common.errmsg.domain_invalid")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderCTCCCloudFaaS, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderCTCCCloudICDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderCTCCCloudICDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderCTCCCloudICDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderCTCCCloudLVDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderCTCCCloudLVDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: { if (!isDomain(values.domain!)) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderCTCCCloudLVDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderCdnfly.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_WEBSITE = "website" as const; const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; const BizDeployNodeConfigFieldsProviderCdnfly = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { resourceType: RESOURCE_TYPE_WEBSITE, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ resourceType: z.literal([RESOURCE_TYPE_WEBSITE, RESOURCE_TYPE_CERTIFICATE], t("workflow_node.deploy.form.shared_resource_type.placeholder")), siteId: z.union([z.string(), z.number().int()]).nullish(), certificateId: z.union([z.string(), z.number().int()]).nullish(), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_WEBSITE: { const scSiteId = z.coerce.number().int().positive(); if (!scSiteId.safeParse(values.siteId).success) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.cdnfly_site_id.placeholder"), path: ["siteId"], }); } } break; case RESOURCE_TYPE_CERTIFICATE: { const scCertificateId = z.coerce.number().int().positive(); if (!scCertificateId.safeParse(values.certificateId).success) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.cdnfly_certificate_id.placeholder"), path: ["certificateId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderCdnfly, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderDogeCloudCDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderDogeCloudCDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: { if (!isDomain(values.domain!)) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderDogeCloudCDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderFlexCDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; const BizDeployNodeConfigFieldsProviderFlexCDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> ); }; const getInitialValues = (): Nullish>> => { return { resourceType: RESOURCE_TYPE_CERTIFICATE, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, t("workflow_node.deploy.form.shared_resource_type.placeholder")), certificateId: z.union([z.string(), z.number().int()]).nullish(), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_CERTIFICATE: { const scCertificateId = z.coerce.number().int().positive(); if (!scCertificateId.safeParse(values.certificateId).success) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.flexcdn_certificate_id.placeholder"), path: ["certificateId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderFlexCDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderFlyIO.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderFlyIO = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> ); }; const getInitialValues = (): Nullish>> => { return { appName: "", domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ appName: z.string().nonempty(t("workflow_node.deploy.form.flyio_app_name.placeholder")), domain: z.string().refine((v) => isDomain(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderFlyIO, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderGcoreCDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderGcoreCDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { resourceId: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ resourceId: z.union([z.string(), z.number().int()]).refine((v) => { return /^\d+$/.test(v + "") && +v > 0; }, t("workflow_node.deploy.form.gcore_cdn_resource_id.placeholder")), certificateId: z .union([z.string(), z.number().int()]) .nullish() .refine((v) => { if (!v) return true; return /^\d+$/.test(v + "") && +v > 0; }, t("workflow_node.deploy.form.gcore_cdn_certificate_id.placeholder")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderGcoreCDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderGoEdge.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; const BizDeployNodeConfigFieldsProviderGoEdge = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> ); }; const getInitialValues = (): Nullish>> => { return { resourceType: RESOURCE_TYPE_CERTIFICATE, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, t("workflow_node.deploy.form.goedge_resource_type.placeholder")), certificateId: z.union([z.string(), z.number().int()]).nullish(), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_CERTIFICATE: { const scCertificateId = z.coerce.number().int().positive(); if (!scCertificateId.safeParse(values.certificateId).success) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.goedge_certificate_id.placeholder"), path: ["certificateId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderGoEdge, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderHuaweiCloudCDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderHuaweiCloudCDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> } > ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { region: "", domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ region: z.string().nonempty(t("workflow_node.deploy.form.huaweicloud_cdn_region.placeholder")), domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderHuaweiCloudCDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderHuaweiCloudELB.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; const RESOURCE_TYPE_LISTENER = "listener" as const; const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; const BizDeployNodeConfigFieldsProviderHuaweiCloudELB = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> } > } > } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", resourceType: RESOURCE_TYPE_LISTENER, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ region: z.string().nonempty(t("workflow_node.deploy.form.huaweicloud_elb_region.placeholder")), resourceType: z.literal( [RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER, RESOURCE_TYPE_CERTIFICATE], t("workflow_node.deploy.form.shared_resource_type.placeholder") ), loadbalancerId: z.string().nullish(), listenerId: z.string().nullish(), certificateId: z.string().nullish(), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_LOADBALANCER: { if (!values.loadbalancerId?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.huaweicloud_elb_loadbalancer_id.placeholder"), path: ["loadbalancerId"], }); } } break; case RESOURCE_TYPE_LISTENER: { if (!values.listenerId?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.huaweicloud_elb_listener_id.placeholder"), path: ["listenerId"], }); } } break; case RESOURCE_TYPE_CERTIFICATE: { if (!values.certificateId?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.huaweicloud_elb_certificate_id.placeholder"), path: ["certificateId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderHuaweiCloudELB, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderHuaweiCloudOBS.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderHuaweiCloudOBS = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", bucket: "", domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ region: z.string().nonempty(t("workflow_node.deploy.form.huaweicloud_obs_region.placeholder")), bucket: z.string().nonempty(t("workflow_node.deploy.form.huaweicloud_obs_bucket.placeholder")), domain: z.string().refine((v) => isDomain(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderHuaweiCloudOBS, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderHuaweiCloudWAF.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_CLOUDSERVER = "cloudserver" as const; const RESOURCE_TYPE_PREMIUMHOST = "premiumhost" as const; const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; const BizDeployNodeConfigFieldsProviderHuaweiCloudWAF = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ region: z.string().nonempty(t("workflow_node.deploy.form.huaweicloud_waf_region.placeholder")), resourceType: z.literal( [RESOURCE_TYPE_CLOUDSERVER, RESOURCE_TYPE_PREMIUMHOST, RESOURCE_TYPE_CERTIFICATE], t("workflow_node.deploy.form.shared_resource_type.placeholder") ), certificateId: z.string().nullish(), domain: z.string().nullish(), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_CLOUDSERVER: case RESOURCE_TYPE_PREMIUMHOST: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.huaweicloud_waf_domain.placeholder"), path: ["domain"], }); } } break; case RESOURCE_TYPE_CERTIFICATE: { if (!values.certificateId?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.huaweicloud_waf_certificate_id.placeholder"), path: ["certificateId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderHuaweiCloudWAF, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderJDCloudALB.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; const RESOURCE_TYPE_LISTENER = "listener" as const; const BizDeployNodeConfigFieldsProviderJDCloudALB = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { regionId: "", resourceType: RESOURCE_TYPE_LISTENER, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ regionId: z.string().nonempty(t("workflow_node.deploy.form.jdcloud_alb_region_id.placeholder")), resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t("workflow_node.deploy.form.shared_resource_type.placeholder")), loadbalancerId: z.string().nullish(), listenerId: z.string().nullish(), domain: z .string() .nullish() .refine((v) => { if (!v) return true; return isDomain(v, { allowWildcard: true }); }, t("common.errmsg.domain_invalid")), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_LOADBALANCER: { if (!values.loadbalancerId?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.jdcloud_alb_loadbalancer_id.placeholder"), path: ["loadbalancerId"], }); } } break; case RESOURCE_TYPE_LISTENER: { if (!values.listenerId?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.jdcloud_alb_listener_id.placeholder"), path: ["listenerId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderJDCloudALB, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderJDCloudCDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderJDCloudCDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderJDCloudCDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderJDCloudLive.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderJDCloudLive = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: { if (!isDomain(values.domain!)) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderJDCloudLive, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderJDCloudVOD.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderJDCloudVOD = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: { if (!isDomain(values.domain!)) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderJDCloudVOD, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderKong.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import Tips from "@/components/Tips"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; const BizDeployNodeConfigFieldsProviderKong = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> } /> } > ); }; const getInitialValues = (): Nullish>> => { return { resourceType: RESOURCE_TYPE_CERTIFICATE, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, t("workflow_node.deploy.form.shared_resource_type.placeholder")), workspace: z.string().nullish(), certificateId: z.string().nullish(), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_CERTIFICATE: { if (!values.certificateId?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.kong_certificate_id.placeholder"), path: ["certificateId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderKong, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderKsyunCDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_DOMAIN = "domain" as const; const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderKsyunCDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { resourceType: RESOURCE_TYPE_DOMAIN, domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ resourceType: z.literal([RESOURCE_TYPE_DOMAIN, RESOURCE_TYPE_CERTIFICATE], t("workflow_node.deploy.form.shared_resource_type.placeholder")), domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), certificateId: z.string().nullish(), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_DOMAIN: { if (!values.domainMatchPattern) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder"), path: ["domainMatchPattern"], }); } switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } break; case RESOURCE_TYPE_CERTIFICATE: { if (!values.certificateId) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.ksyun_cdn_certificate_id.placeholder"), path: ["certificateId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderKsyunCDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderKubernetesSecret.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import CodeTextInput from "@/components/CodeTextInput"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderKubernetesSecret = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const handleSecretAnnotationsBlur = () => { let value = formInst.getFieldValue([parentNamePath, "secretAnnotations"]); value = value.trim(); value = value.replace(/(? { let value = formInst.getFieldValue([parentNamePath, "secretLabels"]); value = value.trim(); value = value.replace(/(? } > } > } > } > } > } > } > ); }; const getInitialValues = (): Nullish>> => { return { namespace: "default", secretType: "kubernetes.io/tls", secretDataKeyForCrt: "tls.crt", secretDataKeyForKey: "tls.key", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ namespace: z.string().nonempty(t("workflow_node.deploy.form.k8s_namespace.placeholder")), secretName: z.string().nonempty(t("workflow_node.deploy.form.k8s_secret_name.placeholder")), secretType: z.string().nonempty(t("workflow_node.deploy.form.k8s_secret_type.placeholder")), secretDataKeyForCrt: z.string().nonempty(t("workflow_node.deploy.form.k8s_secret_data_key_for_crt.placeholder")), secretDataKeyForKey: z.string().nonempty(t("workflow_node.deploy.form.k8s_secret_data_key_for_key.placeholder")), secretAnnotations: z .string() .nullish() .refine((v) => { if (!v) return true; const lines = v.split(/\r?\n/); for (const line of lines) { if (line.split(":").length < 2) { return false; } } return true; }, t("workflow_node.deploy.form.k8s_secret_annotations.errmsg.invalid")), secretLabels: z .string() .nullish() .refine((v) => { if (!v) return true; const lines = v.split(/\r?\n/); for (const line of lines) { if (line.split(":").length < 2) { return false; } } return true; }, t("workflow_node.deploy.form.k8s_secret_labels.errmsg.invalid")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderKubernetesSecret, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderLeCDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; const BizDeployNodeConfigFieldsProviderLeCDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { resourceType: RESOURCE_TYPE_CERTIFICATE, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, t("workflow_node.deploy.form.shared_resource_type.placeholder")), certificateId: z.union([z.string(), z.number().int()]).nullish(), clientId: z.union([z.string(), z.number().int()]).nullish(), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_CERTIFICATE: { const scCertificateId = z.coerce.number().int().positive(); if (!scCertificateId.safeParse(values.certificateId).success) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.lecdn_certificate_id.placeholder"), path: ["certificateId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderLeCDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderLocal.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { IconBulb, IconChevronDown } from "@tabler/icons-react"; import { Button, Divider, Form, Input, Popover, Select, Space } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import CodeTextInput from "@/components/CodeTextInput"; import PresetScriptTemplatesPopselect from "@/components/preset/PresetScriptTemplatesPopselect"; import Show from "@/components/Show"; import Tips from "@/components/Tips"; import { CERTIFICATE_FORMATS } from "@/domain/certificate"; import { useFormNestedFieldsContext } from "./_context"; const FORMAT_PEM = CERTIFICATE_FORMATS.PEM; const FORMAT_PFX = CERTIFICATE_FORMATS.PFX; const FORMAT_JKS = CERTIFICATE_FORMATS.JKS; const SHELLENV_SH = "sh" as const; const SHELLENV_CMD = "cmd" as const; const SHELLENV_POWERSHELL = "powershell" as const; export const initPresetScript = ( key: "sh_backup_files" | "ps_backup_files" | "sh_reload_nginx" | "ps_binding_iis" | "ps_binding_netsh" | "ps_binding_rdp", params?: { certPath?: string; certPathForServerOnly?: string; certPathForIntermediaOnly?: string; keyPath?: string; pfxPassword?: string; jksAlias?: string; jksKeypass?: string; jksStorepass?: string; } ) => { switch (key) { case "sh_backup_files": return `# 请将以下路径替换为实际值 cp "${params?.certPath || ""}" "${params?.certPath || ""}.bak" 2>/dev/null || : cp "${params?.keyPath || ""}" "${params?.keyPath || ""}.bak" 2>/dev/null || : `.trim(); case "ps_backup_files": return `# 请将以下路径替换为实际值 if (Test-Path -Path "${params?.certPath || ""}" -PathType Leaf) { Copy-Item -Path "${params?.certPath || ""}" -Destination "${params?.certPath || ""}.bak" -Force } if (Test-Path -Path "${params?.keyPath || ""}" -PathType Leaf) { Copy-Item -Path "${params?.keyPath || ""}" -Destination "${params?.keyPath || ""}.bak" -Force } `.trim(); case "sh_reload_nginx": return `# *** 需要 root 权限 *** sudo service nginx reload `.trim(); case "ps_binding_iis": return `# *** 需要管理员权限 *** # 请将以下变量替换为实际值 $pfxPath = "\${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}" # PFX 文件路径(与表单中保持一致) $pfxPassword = "\${CERTIMATE_DEPLOYER_CMDVAR_PFX_PASSWORD}" # PFX 密码(与表单中保持一致) $siteName = "" # IIS 网站名称 $domain = "" # 域名 $ipaddr = "" # 绑定 IP,“*”表示所有 IP 绑定 $port = "" # 绑定端口 # 导入证书到本地计算机的个人存储区 $cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable # 获取 Thumbprint $thumbprint = $cert.Thumbprint # 导入 WebAdministration 模块 Import-Module WebAdministration # 检查是否已存在 HTTPS 绑定 $existingBinding = Get-WebBinding -Name "$siteName" -Protocol "https" -Port $port -HostHeader "$domain" -ErrorAction SilentlyContinue if (!$existingBinding) { # 添加新的 HTTPS 绑定 New-WebBinding -Name "$siteName" -Protocol "https" -Port $port -IPAddress "$ipaddr" -HostHeader "$domain" } # 获取绑定对象 $binding = Get-WebBinding -Name "$siteName" -Protocol "https" -Port $port -IPAddress "$ipaddr" -HostHeader "$domain" # 绑定 SSL 证书 $binding.AddSslCertificate($thumbprint, "My") # 删除目录下的证书文件 Remove-Item -Path "$pfxPath" -Force `.trim(); case "ps_binding_netsh": return `# *** 需要管理员权限 *** # 请将以下变量替换为实际值 $pfxPath = "\${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}" # PFX 文件路径(与表单中保持一致) $pfxPassword = "\${CERTIMATE_DEPLOYER_CMDVAR_PFX_PASSWORD}" # PFX 密码(与表单中保持一致) $ipaddr = "" # 绑定 IP,“0.0.0.0”表示所有 IP 绑定,可填入域名 $port = "" # 绑定端口 # 导入证书到本地计算机的个人存储区 $addr = $ipaddr + ":" + $port $cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable # 获取 Thumbprint $thumbprint = $cert.Thumbprint # 检测端口是否绑定证书,如绑定则删除绑定 $isExist = netsh http show sslcert ipport=$addr if ($isExist -like "*$addr*"){ netsh http delete sslcert ipport=$addr } # 绑定到端口 netsh http add sslcert ipport=$addr certhash=$thumbprint # 删除目录下的证书文件 Remove-Item -Path "$pfxPath" -Force `.trim(); case "ps_binding_rdp": return `# *** 需要管理员权限 *** # 请将以下变量替换为实际值 $pfxPath = "\${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}" # PFX 文件路径(与表单中保持一致) $pfxPassword = "\${CERTIMATE_DEPLOYER_CMDVAR_PFX_PASSWORD}" # PFX 密码(与表单中保持一致) # 导入证书到本地计算机的个人存储区 $cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable # 获取 Thumbprint $thumbprint = $cert.Thumbprint # 绑定到 RDP $rdpCertPath = "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp" Set-ItemProperty -Path $rdpCertPath -Name "SSLCertificateSHA1Hash" -Value "$thumbprint" `.trim(); } }; const BizDeployNodeConfigFieldsProviderLocal = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldFormat = Form.useWatch([parentNamePath, "format"], formInst); const fieldCertPath = Form.useWatch([parentNamePath, "certPath"], formInst); const handleFormatSelect = (value: string) => { if (fieldFormat === value) return; switch (value) { case FORMAT_PEM: { if (/(.pfx|.jks)$/.test(fieldCertPath)) { formInst.setFieldValue([parentNamePath, "certPath"], fieldCertPath.replace(/(.pfx|.jks)$/, ".crt")); } } break; case FORMAT_PFX: { if (/(.crt|.jks)$/.test(fieldCertPath)) { formInst.setFieldValue([parentNamePath, "certPath"], fieldCertPath.replace(/(.crt|.jks)$/, ".pfx")); } } break; case FORMAT_JKS: { if (/(.crt|.pfx)$/.test(fieldCertPath)) { formInst.setFieldValue([parentNamePath, "certPath"], fieldCertPath.replace(/(.crt|.pfx)$/, ".jks")); } } break; } }; const handlePresetPreScriptClick = (key: string) => { switch (key) { case "sh_backup_files": case "ps_backup_files": { const presetScriptParams = { certPath: formInst.getFieldValue([parentNamePath, "certPath"]), keyPath: formInst.getFieldValue([parentNamePath, "keyPath"]), }; formInst.setFieldValue([parentNamePath, "shellEnv"], SHELLENV_SH); formInst.setFieldValue([parentNamePath, "preCommand"], initPresetScript(key, presetScriptParams)); } break; } }; const handlePresetPostScriptClick = (key: string) => { switch (key) { case "sh_reload_nginx": { formInst.setFieldValue([parentNamePath, "shellEnv"], SHELLENV_SH); formInst.setFieldValue([parentNamePath, "postCommand"], initPresetScript(key)); } break; case "ps_binding_iis": case "ps_binding_netsh": case "ps_binding_rdp": { const presetScriptParams = { certPath: formInst.getFieldValue([parentNamePath, "certPath"]), pfxPassword: formInst.getFieldValue([parentNamePath, "pfxPassword"]), }; formInst.setFieldValue([parentNamePath, "shellEnv"], SHELLENV_POWERSHELL); formInst.setFieldValue([parentNamePath, "postCommand"], initPresetScript(key, presetScriptParams)); } break; } }; return ( <> } /> } > } > } > } > } > ); }; const getInitialValues = (): Nullish>> => { return { hostId: "", domainId: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ hostId: z.union([ z.string().nonempty(t("workflow_node.deploy.form.mohua_mvh_host_id.placeholder")), z.number().int(t("workflow_node.deploy.form.mohua_mvh_host_id.placeholder")), ]), domainId: z.union([ z.string().nonempty(t("workflow_node.deploy.form.mohua_mvh_domain_id.placeholder")), z.number().int(t("workflow_node.deploy.form.mohua_mvh_domain_id.placeholder")), ]), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderMohuaMVH, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderNetlify.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_WEBSITE = "website" as const; const BizDeployNodeConfigFieldsProviderNetlify = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> ); }; const getInitialValues = (): Nullish>> => { return { resourceType: RESOURCE_TYPE_WEBSITE, siteId: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ resourceType: z.literal(RESOURCE_TYPE_WEBSITE, t("workflow_node.deploy.form.cpanel_resource_type.placeholder")), siteId: z.string().nullish(), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_WEBSITE: { const scSiteId = z.string().nonempty(); if (!scSiteId.safeParse(values.siteId).success) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.netlify_site_id.placeholder"), path: ["siteId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderNetlify, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderNginxProxyManager.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_HOST = "host" as const; const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; const HOST_MATCH_PATTERN_SPECIFIED = "specified" as const; const HOST_MATCH_PATTERN_CERTSAN = "certsan" as const; const HOST_TYPE_PROXY = "proxy" as const; const HOST_TYPE_REDIRECTION = "redirection" as const; const HOST_TYPE_STREAM = "stream" as const; const HOST_TYPE_DEAD = "dead" as const; const BizDeployNodeConfigFieldsProviderNginxProxyManager = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); const fieldHostMatchPattern = Form.useWatch([parentNamePath, "hostMatchPattern"], { form: formInst, preserve: true }); return ( <> ({ value: s, label: t(`workflow_node.deploy.form.nginxproxymanager_host_type.option.${s}.label`), }))} placeholder={t("workflow_node.deploy.form.nginxproxymanager_host_type.placeholder")} /> } > } > ); }; const getInitialValues = (): Nullish>> => { return { resourceType: RESOURCE_TYPE_HOST, hostMatchPattern: HOST_MATCH_PATTERN_SPECIFIED, hostType: HOST_TYPE_PROXY, hostId: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ resourceType: z.literal([RESOURCE_TYPE_HOST, RESOURCE_TYPE_CERTIFICATE], t("workflow_node.deploy.form.shared_resource_type.placeholder")), hostMatchPattern: z.string().nullish(), hostType: z.string().nullish(), hostId: z.union([z.string(), z.number().int()]).nullish(), certificateId: z.union([z.string(), z.number().int()]).nullish(), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_HOST: { if (values.hostMatchPattern) { switch (values.hostMatchPattern) { case HOST_MATCH_PATTERN_SPECIFIED: { const scHostType = z.string().nonempty(); if (!scHostType.safeParse(values.hostType).success) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.nginxproxymanager_host_type.placeholder"), path: ["hostType"], }); } const scHostId = z.coerce.number().int().positive(); if (!scHostId.safeParse(values.hostId).success) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.nginxproxymanager_host_id.placeholder"), path: ["hostId"], }); } } break; } } else { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.nginxproxymanager_host_match_pattern.placeholder"), path: ["hostMatchPattern"], }); } } break; case RESOURCE_TYPE_CERTIFICATE: { const scCertificateId = z.coerce.number().int().positive(); if (!scCertificateId.safeParse(values.certificateId).success) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.nginxproxymanager_certificate_id.placeholder"), path: ["hostId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderNginxProxyManager, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderProxmoxVE.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Switch } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Tips from "@/components/Tips"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderProxmoxVE = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } /> ); }; const getInitialValues = (): Nullish>> => { return { nodeName: "", autoRestart: true, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ nodeName: z.string().nonempty(t("workflow_node.deploy.form.proxmoxve_node_name.placeholder")), autoRestart: z.boolean().nullish(), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderProxmoxVE, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderQiniuCDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderQiniuCDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderQiniuCDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderQiniuKodo.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderQiniuKodo = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> ); }; const getInitialValues = (): Nullish>> => { return { bucket: "", domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ bucket: z.string().nonempty(t("workflow_node.deploy.form.qiniu_kodo_domain.placeholder")), domain: z.string().refine((v) => isDomain(v), t("common.errmsg.domain_invalid")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderQiniuKodo, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderQiniuPili.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderQiniuPili = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> } > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { hub: "", domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ hub: z.string().nonempty(t("workflow_node.deploy.form.qiniu_pili_hub.placeholder")), domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: { if (!isDomain(values.domain!)) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderQiniuPili, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderRainYunRCDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const BizDeployNodeConfigFieldsProviderRainYunRCDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> } > ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { instanceId: "", domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ instanceId: z.union([z.string(), z.number().int()]).nullish(), domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderRainYunRCDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderRainYunSSLCenter.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderRainYunSSLCenter = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return {}; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t: _ } = i18n; return z.object({ certificateId: z.union([z.string(), z.number().int()]).nullish(), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderRainYunSSLCenter, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderRatPanel.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import MultipleSplitValueInput from "@/components/MultipleSplitValueInput"; import Show from "@/components/Show"; import Tips from "@/components/Tips"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_WEBSITE = "website" as const; const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; const MULTIPLE_INPUT_SEPARATOR = ";"; const BizDeployNodeConfigFieldsProviderRatPanel = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> } /> ); }; const getInitialValues = (): Nullish>> => { return { resourceType: RESOURCE_TYPE_WEBSITE, siteNames: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ resourceType: z.literal([RESOURCE_TYPE_WEBSITE, RESOURCE_TYPE_CERTIFICATE], t("workflow_node.deploy.form.cpanel_resource_type.placeholder")), siteNames: z.string().nullish(), certificateId: z.union([z.string(), z.number().int()]).nullish(), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_WEBSITE: { const scSiteNames = z .string() .nonempty() .refine((v) => { if (!v) return false; return v.split(MULTIPLE_INPUT_SEPARATOR).every((s) => !!s.trim()); }); if (!scSiteNames.safeParse(values.siteNames).success) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.ratpanel_site_names.placeholder"), path: ["siteNames"], }); } } break; case RESOURCE_TYPE_CERTIFICATE: { const scCertificateId = z.coerce.number().int().positive(); if (!scCertificateId.safeParse(values.certificateId).success) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.ratpanel_certificate_id.placeholder"), path: ["certificateId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderRatPanel, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderS3.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { CERTIFICATE_FORMATS } from "@/domain/certificate"; import { useFormNestedFieldsContext } from "./_context"; const FORMAT_PEM = CERTIFICATE_FORMATS.PEM; const FORMAT_PFX = CERTIFICATE_FORMATS.PFX; const FORMAT_JKS = CERTIFICATE_FORMATS.JKS; const BizDeployNodeConfigFieldsProviderS3 = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldFormat = Form.useWatch([parentNamePath, "format"], formInst); const fieldCertPath = Form.useWatch([parentNamePath, "certObjectKey"], formInst); const handleFormatSelect = (value: string) => { if (fieldFormat === value) return; switch (value) { case FORMAT_PEM: { if (/(.pfx|.jks)$/.test(fieldCertPath)) { formInst.setFieldValue([parentNamePath, "certObjectKey"], fieldCertPath.replace(/(.pfx|.jks)$/, ".crt")); } } break; case FORMAT_PFX: { if (/(.crt|.jks)$/.test(fieldCertPath)) { formInst.setFieldValue([parentNamePath, "certObjectKey"], fieldCertPath.replace(/(.crt|.jks)$/, ".pfx")); } } break; case FORMAT_JKS: { if (/(.crt|.pfx)$/.test(fieldCertPath)) { formInst.setFieldValue([parentNamePath, "certObjectKey"], fieldCertPath.replace(/(.crt|.pfx)$/, ".jks")); } } break; } }; return ( <> } > } > } > } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", bucket: "", format: FORMAT_PEM, keyObjectKey: ".certimate/cert.key", certObjectKey: ".certimate/cert.crt", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ region: z.string().nonempty(t("workflow_node.deploy.form.s3_region.placeholder")), bucket: z.string().nonempty(t("workflow_node.deploy.form.s3_bucket.placeholder")), format: z.literal([FORMAT_PEM, FORMAT_PFX, FORMAT_JKS], t("workflow_node.deploy.form.s3_format.placeholder")), keyObjectKey: z .string() .max(256, t("common.errmsg.string_max", { max: 256 })) .nullish(), certObjectKey: z .string() .min(1, t("workflow_node.deploy.form.s3_cert_object_key.placeholder")) .max(256, t("common.errmsg.string_max", { max: 256 })), certObjectKeyForServerOnly: z .string() .max(256, t("common.errmsg.string_max", { max: 256 })) .nullish(), certObjectKeyForIntermediaOnly: z .string() .max(256, t("common.errmsg.string_max", { max: 256 })) .nullish(), pfxPassword: z.string().nullish(), jksAlias: z.string().nullish(), jksKeypass: z.string().nullish(), jksStorepass: z.string().nullish(), }) .superRefine((values, ctx) => { switch (values.format) { case FORMAT_PEM: { if (!values.keyObjectKey?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.s3_key_object_key.placeholder"), path: ["keyObjectKey"], }); } } break; case FORMAT_PFX: { if (!values.pfxPassword?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.s3_pfx_password.placeholder"), path: ["pfxPassword"], }); } } break; case FORMAT_JKS: { if (!values.jksAlias?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.s3_jks_alias.placeholder"), path: ["jksAlias"], }); } if (!values.jksKeypass?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.s3_jks_keypass.placeholder"), path: ["jksKeypass"], }); } if (!values.jksStorepass?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.s3_jks_storepass.placeholder"), path: ["jksStorepass"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderS3, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderSSH.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { IconBulb, IconChevronDown } from "@tabler/icons-react"; import { Button, Divider, Form, Input, Popover, Select, Space, Switch } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import CodeTextInput from "@/components/CodeTextInput"; import PresetScriptTemplatesPopselect from "@/components/preset/PresetScriptTemplatesPopselect"; import Show from "@/components/Show"; import { CERTIFICATE_FORMATS } from "@/domain/certificate"; import { useFormNestedFieldsContext } from "./_context"; import { initPresetScript as _initPresetScript } from "./BizDeployNodeConfigFieldsProviderLocal"; const FORMAT_PEM = CERTIFICATE_FORMATS.PEM; const FORMAT_PFX = CERTIFICATE_FORMATS.PFX; const FORMAT_JKS = CERTIFICATE_FORMATS.JKS; const initPresetScript = ( key: Parameters[0] | "sh_replace_synologydsm_ssl" | "sh_replace_fnos_ssl" | "sh_replace_qnap_ssl", params?: Parameters[1] ) => { switch (key) { case "sh_replace_synologydsm_ssl": return `# *** 需要 root 权限 *** # 注意仅支持替换证书,需本身已开启过一次 HTTPS # 脚本参考 https://github.com/catchdave/ssl-certs/blob/main/replace_synology_ssl_certs.sh # 请将以下变量替换为实际值 $tmpFullchainPath = "\${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}" # 证书文件路径(与表单中保持一致) $tmpCertPath = "\${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_SERVER_PATH}" # 服务器证书文件路径(与表单中保持一致) $tmpKeyPath = "\${CERTIMATE_DEPLOYER_CMDVAR_PRIVATEKEY_PATH}" # 私钥文件路径(与表单中保持一致) DEBUG=1 error_exit() { echo "[ERROR] $1"; exit 1; } warn() { echo "[WARN] $1"; } info() { echo "[INFO] $1"; } debug() { [[ "\${DEBUG}" ]] && echo "[DEBUG] $1"; } certs_src_dir="/usr/syno/etc/certificate/system/default" target_cert_dirs=( "/usr/syno/etc/certificate/system/FQDN" "/usr/local/etc/certificate/ScsiTarget/pkg-scsi-plugin-server/" "/usr/local/etc/certificate/SynologyDrive/SynologyDrive/" "/usr/local/etc/certificate/WebDAVServer/webdav/" "/usr/local/etc/certificate/ActiveBackup/ActiveBackup/" "/usr/syno/etc/certificate/smbftpd/ftpd/") # 获取证书目录 default_dir_name=$(/dev/null && /usr/syno/bin/synopkg restart ScsiTarget /usr/syno/bin/synopkg is_onoff SynologyDrive 1>/dev/null && /usr/syno/bin/synopkg restart SynologyDrive /usr/syno/bin/synopkg is_onoff WebDAVServer 1>/dev/null && /usr/syno/bin/synopkg restart WebDAVServer /usr/syno/bin/synopkg is_onoff ActiveBackup 1>/dev/null && /usr/syno/bin/synopkg restart ActiveBackup if ! /usr/syno/bin/synow3tool --gen-all && sudo /usr/syno/bin/synosystemctl restart nginx; then warn "nginx failed to restart" fi info "Completed" `.trim(); case "sh_replace_fnos_ssl": return `# *** 需要 root 权限 *** # 注意仅支持替换证书,需本身已开启过一次 HTTPS # 脚本参考 https://github.com/lfgyx/fnos_certificate_update/blob/main/src/update_cert.sh # 请将以下变量替换为实际值 # 飞牛证书实际存放路径请在 \`/usr/trim/etc/network_cert_all.conf\` 中查看,注意不要修改文件名 $tmpFullchainPath = "\${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}" # 证书文件路径(与表单中保持一致) $tmpCertPath = "\${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_SERVER_PATH}" # 服务器证书文件路径(与表单中保持一致) $tmpKeyPath = "\${CERTIMATE_DEPLOYER_CMDVAR_PRIVATEKEY_PATH}" # 私钥文件路径(与表单中保持一致) $fnFullchainPath = "/usr/trim/var/trim_connect/ssls/example.com/1234567890/fullchain.crt" # 飞牛证书文件路径 $fnCertPath = "/usr/trim/var/trim_connect/ssls/example.com/1234567890/example.com.crt" # 飞牛服务器证书文件路径 $fnKeyPath = "/usr/trim/var/trim_connect/ssls/example.com/1234567890/example.com.key" # 飞牛私钥文件路径 $domain = "" # 域名 # 复制文件 cp -rf "$tmpFullchainPath" "$fnFullchainPath" cp -rf "$tmpCertPath" "$fnCertPath" cp -rf "$tmpKeyPath" "$fnKeyPath" chmod 755 "$fnFullchainPath" chmod 755 "$fnCertPath" chmod 755 "$fnKeyPath" # 更新数据库 NEW_EFFECT_DATE=$(openssl x509 -startdate -noout -in "$fnCertPath" | sed "s/^.*=\\(.*\\)$/\\1/") NEW_EFFECT_TIMESTAMP=$(date -d "$NEW_EFFECT_DATE" +%s%3N) NEW_EXPIRY_DATE=$(openssl x509 -enddate -noout -in "$fnCertPath" | sed "s/^.*=\\(.*\\)$/\\1/") NEW_EXPIRY_TIMESTAMP=$(date -d "$NEW_EXPIRY_DATE" +%s%3N) psql -U postgres -d trim_connect -c "UPDATE cert SET valid_from=$NEW_EFFECT_TIMESTAMP, valid_to=$NEW_EXPIRY_TIMESTAMP WHERE domain='$domain'" # 重启服务 systemctl restart webdav.service systemctl restart smbftpd.service systemctl restart trim_nginx.service `.trim(); case "sh_replace_qnap_ssl": return `# *** 需要 root 权限 *** # 注意仅支持替换证书,需本身已开启过一次 HTTPS # 请将以下变量替换为实际值 $tmpFullchainPath = "\${CERTIMATE_DEPLOYER_CMDVAR_CERTIFICATE_PATH}"}" # 证书文件路径(与表单中保持一致) $tmpKeyPath = "\${CERTIMATE_DEPLOYER_CMDVAR_PRIVATEKEY_PATH}" # 私钥文件路径(与表单中保持一致) # 复制文件 cp -rf "$tmpFullchainPath" /etc/stunnel/backup.cert cp -rf "$tmpKeyPath" /etc/stunnel/backup.key cat /etc/stunnel/backup.key > /etc/stunnel/stunnel.pem cat /etc/stunnel/backup.cert >> /etc/stunnel/stunnel.pem chmod 600 /etc/stunnel/backup.cert chmod 600 /etc/stunnel/backup.key chmod 600 /etc/stunnel/stunnel.pem # 重启服务 /etc/init.d/stunnel.sh restart /etc/init.d/reverse_proxy.sh reload `.trim(); } return _initPresetScript(key as Parameters[0], params); }; const BizDeployNodeConfigFieldsProviderSSH = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldFormat = Form.useWatch([parentNamePath, "format"], formInst); const fieldCertPath = Form.useWatch([parentNamePath, "certPath"], formInst); const handleFormatSelect = (value: string) => { if (fieldFormat === value) return; switch (value) { case FORMAT_PEM: { if (/(.pfx|.jks)$/.test(fieldCertPath)) { formInst.setFieldValue([parentNamePath, "certPath"], fieldCertPath.replace(/(.pfx|.jks)$/, ".crt")); } } break; case FORMAT_PFX: { if (/(.crt|.jks)$/.test(fieldCertPath)) { formInst.setFieldValue([parentNamePath, "certPath"], fieldCertPath.replace(/(.crt|.jks)$/, ".pfx")); } } break; case FORMAT_JKS: { if (/(.crt|.pfx)$/.test(fieldCertPath)) { formInst.setFieldValue([parentNamePath, "certPath"], fieldCertPath.replace(/(.crt|.pfx)$/, ".jks")); } } break; } }; const handlePresetPreScriptClick = (key: string) => { switch (key) { case "sh_backup_files": case "ps_backup_files": { const presetScriptParams = { certPath: formInst.getFieldValue([parentNamePath, "certPath"]), keyPath: formInst.getFieldValue([parentNamePath, "keyPath"]), }; formInst.setFieldValue([parentNamePath, "preCommand"], initPresetScript(key, presetScriptParams)); } break; } }; const handlePresetPostScriptClick = (key: string) => { switch (key) { case "sh_reload_nginx": { formInst.setFieldValue([parentNamePath, "postCommand"], initPresetScript(key)); } break; case "sh_replace_synologydsm_ssl": case "sh_replace_fnos_ssl": case "sh_replace_qnap_ssl": { const presetScriptParams = { certPath: formInst.getFieldValue([parentNamePath, "certPath"]), certPathForServerOnly: formInst.getFieldValue([parentNamePath, "certPathForServerOnly"]), certPathForIntermediaOnly: formInst.getFieldValue([parentNamePath, "certPathForIntermediaOnly"]), keyPath: formInst.getFieldValue([parentNamePath, "keyPath"]), }; formInst.setFieldValue([parentNamePath, "postCommand"], initPresetScript(key, presetScriptParams)); } break; case "ps_binding_iis": case "ps_binding_netsh": case "ps_binding_rdp": { const presetScriptParams = { certPath: formInst.getFieldValue([parentNamePath, "certPath"]), pfxPassword: formInst.getFieldValue([parentNamePath, "pfxPassword"]), }; formInst.setFieldValue([parentNamePath, "postCommand"], initPresetScript(key, presetScriptParams)); } break; } }; return ( <> } > } > } > } > } >
({ key, label: t(`workflow_node.deploy.form.ssh_preset_scripts.${key}`), }))} trigger={["click"]} onSelect={(key, template) => { if (template) { formInst.setFieldValue([parentNamePath, "preCommand"], template.command); } else { handlePresetPreScriptClick(key); } }} >
} size={0}> } mouseEnterDelay={1}> ({ key, label: t(`workflow_node.deploy.form.ssh_preset_scripts.${key}`), }))} trigger={["click"]} onSelect={(key, template) => { if (template) { formInst.setFieldValue([parentNamePath, "postCommand"], template.command); } else { handlePresetPostScriptClick(key); } }} >
); }; const getInitialValues = (): Nullish>> => { return { format: FORMAT_PEM, keyPath: "/etc/ssl/certimate/cert.key", certPath: "/etc/ssl/certimate/cert.crt", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ useSCP: z.boolean().nullish(), format: z.literal([FORMAT_PEM, FORMAT_PFX, FORMAT_JKS], t("workflow_node.deploy.form.ssh_format.placeholder")), keyPath: z .string() .max(256, t("common.errmsg.string_max", { max: 256 })) .nullish(), certPath: z .string() .min(1, t("workflow_node.deploy.form.ssh_cert_path.placeholder")) .max(256, t("common.errmsg.string_max", { max: 256 })), certPathForServerOnly: z .string() .max(256, t("common.errmsg.string_max", { max: 256 })) .nullish(), certPathForIntermediaOnly: z .string() .max(256, t("common.errmsg.string_max", { max: 256 })) .nullish(), pfxPassword: z.string().nullish(), jksAlias: z.string().nullish(), jksKeypass: z.string().nullish(), jksStorepass: z.string().nullish(), preCommand: z .string() .max(20480, t("common.errmsg.string_max", { max: 20480 })) .nullish(), postCommand: z .string() .max(20480, t("common.errmsg.string_max", { max: 20480 })) .nullish(), }) .superRefine((values, ctx) => { switch (values.format) { case FORMAT_PEM: { if (!values.keyPath?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.ssh_key_path.placeholder"), path: ["keyPath"], }); } } break; case FORMAT_PFX: { if (!values.pfxPassword?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.ssh_pfx_password.placeholder"), path: ["pfxPassword"], }); } } break; case FORMAT_JKS: { if (!values.jksAlias?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.ssh_jks_alias.placeholder"), path: ["jksAlias"], }); } if (!values.jksKeypass?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.ssh_jks_keypass.placeholder"), path: ["jksKeypass"], }); } if (!values.jksStorepass?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.ssh_jks_storepass.placeholder"), path: ["jksStorepass"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderSSH, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderSafeLine.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import Tips from "@/components/Tips"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; const BizDeployNodeConfigFieldsProviderSafeLine = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> } /> ); }; const getInitialValues = (): Nullish>> => { return { resourceType: RESOURCE_TYPE_CERTIFICATE, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, t("workflow_node.deploy.form.shared_resource_type.placeholder")), certificateId: z.union([z.string(), z.number().int()]).nullish(), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_CERTIFICATE: { const scCertificateId = z.coerce.number().int().positive(); if (!scCertificateId.safeParse(values.certificateId).success) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.safeline_certificate_id.placeholder"), path: ["certificateId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderSafeLine, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderSynologyDSM.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Switch } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Tips from "@/components/Tips"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderSynologyDSM = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } /> } rules={[formRule]} > ); }; const getInitialValues = (): Nullish>> => { return { isDefault: true, }; }; const getSchema = ({ i18n: _i18n = getI18n() }: { i18n?: ReturnType }) => { return z.object({ certificateIdOrDesc: z.string().nullish(), isDefault: z.boolean().nullish(), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderSynologyDSM, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudCDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderTencentCloudCDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> } > ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ endpoint: z.string().nullish(), domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudCDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudCLB.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; const RESOURCE_TYPE_LISTENER = "listener" as const; const RESOURCE_TYPE_RULEDOMAIN = "ruledomain" as const; const BizDeployNodeConfigFieldsProviderTencentCloudCLB = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> } > } > } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", resourceType: RESOURCE_TYPE_LISTENER, loadbalancerId: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ endpoint: z.string().nullish(), resourceType: z.literal( [RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER, RESOURCE_TYPE_RULEDOMAIN], t("workflow_node.deploy.form.shared_resource_type.placeholder") ), region: z.string().nonempty(t("workflow_node.deploy.form.tencentcloud_clb_region.placeholder")), loadbalancerId: z.string().nonempty(t("workflow_node.deploy.form.tencentcloud_clb_loadbalancer_id.placeholder")), listenerId: z.string().nullish(), domain: z.string().nullish(), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_LISTENER: { if (!values.listenerId?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.tencentcloud_clb_listener_id.placeholder"), path: ["listenerId"], }); } } break; case RESOURCE_TYPE_RULEDOMAIN: { if (!values.listenerId?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.tencentcloud_clb_listener_id.placeholder"), path: ["listenerId"], }); } if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudCLB, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudCOS.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderTencentCloudCOS = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", bucket: "", domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ region: z.string().nonempty(t("workflow_node.deploy.form.tencentcloud_cos_region.placeholder")), bucket: z.string().nonempty(t("workflow_node.deploy.form.tencentcloud_cos_bucket.placeholder")), domain: z.string().refine((v) => isDomain(v), t("common.errmsg.domain_invalid")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudCOS, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudCSS.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderTencentCloudCSS = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> } > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ endpoint: z.string().nullish(), domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: { if (!isDomain(values.domain!)) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudCSS, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudECDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderTencentCloudECDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> } > ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ endpoint: z.string().nullish(), domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudECDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudEO.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio, Switch } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import MultipleSplitValueInput from "@/components/MultipleSplitValueInput"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const MULTIPLE_INPUT_SEPARATOR = ";"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderTencentCloudEO = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> } > } > ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> {t("workflow_node.deploy.form.tencentcloud_eo_enable_multiple_ssl.switch.suffix")} ); }; const getInitialValues = (): Nullish>> => { return { domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, zoneId: "", domains: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ endpoint: z.string().nullish(), zoneId: z.string().nonempty(t("workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder")), domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domains: z.string().nullish(), enableMultipleSSL: z.boolean().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { const valid = values.domains && values.domains.split(MULTIPLE_INPUT_SEPARATOR).every((e) => isDomain(e, { allowWildcard: true })); if (!valid) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domains"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudEO, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudGAAP.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_LISTENER = "listener" as const; const BizDeployNodeConfigFieldsProviderTencentCloudGAAP = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { resourceType: RESOURCE_TYPE_LISTENER, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ endpoint: z.string().nullish(), resourceType: z.literal(RESOURCE_TYPE_LISTENER, t("workflow_node.deploy.form.shared_resource_type.placeholder")), proxyId: z.string().nullish(), listenerId: z.string().nullish(), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_LISTENER: { if (!values.listenerId?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.tencentcloud_gaap_listener_id.placeholder"), path: ["listenerId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudGAAP, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudSCF.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderTencentCloudSCF = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> } > } > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { region: "", domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ endpoint: z.string().nullish(), region: z.string().nonempty(t("workflow_node.deploy.form.tencentcloud_scf_region.placeholder")), domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: { if (!isDomain(values.domain!)) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudSCF, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudSSL.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderTencentCloudSSL = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return {}; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t: _ } = i18n; return z.object({ endpoint: z.string().nullish(), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudSSL, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudSSLDeploy.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { AutoComplete, Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import MultipleSplitValueInput from "@/components/MultipleSplitValueInput"; import Tips from "@/components/Tips"; import { matchSearchOption } from "@/utils/search"; import { useFormNestedFieldsContext } from "./_context"; const MULTIPLE_INPUT_SEPARATOR = ";"; const BizDeployNodeConfigFieldsProviderTencentCloudSSLDeploy = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } /> } > } > } > ({ value }))} placeholder={t("workflow_node.deploy.form.tencentcloud_ssldeploy_resource_product.placeholder")} showSearch={{ filterOption: (inputValue, option) => matchSearchOption(inputValue, option!), }} /> } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", resourceProduct: "", resourceIds: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ endpoint: z.string().nullish(), region: z.string().nonempty(t("workflow_node.deploy.form.tencentcloud_ssldeploy_region.placeholder")), resourceProduct: z.string().nonempty(t("workflow_node.deploy.form.tencentcloud_ssldeploy_resource_product.placeholder")), resourceIds: z.string().refine((v) => { if (!v) return false; return v.split(MULTIPLE_INPUT_SEPARATOR).every((e) => /^[A-Za-z0-9*._\-|]+$/.test(e)); }, t("workflow_node.deploy.form.tencentcloud_ssldeploy_resource_ids.errmsg.invalid")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudSSLDeploy, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudSSLUpdate.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Switch } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import MultipleSplitValueInput from "@/components/MultipleSplitValueInput"; import Tips from "@/components/Tips"; import { useFormNestedFieldsContext } from "./_context"; const MULTIPLE_INPUT_SEPARATOR = ";"; const BizDeployNodeConfigFieldsProviderTencentCloudSSLUpdate = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } /> } > } > } > } > } > ); }; const getInitialValues = (): Nullish>> => { return { certificateId: "", resourceProducts: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ endpoint: z.string().nullish(), certificateId: z.string().nonempty(t("workflow_node.deploy.form.tencentcloud_sslupdate_certificate_id.placeholder")), resourceProducts: z.string().refine((v) => { if (!v) return false; return v.split(MULTIPLE_INPUT_SEPARATOR).every((e) => !!e.trim()); }, t("workflow_node.deploy.form.tencentcloud_sslupdate_resource_products.placeholder")), resourceRegions: z .string() .nullish() .refine((v) => { if (!v) return true; return v.split(MULTIPLE_INPUT_SEPARATOR).every((e) => !!e.trim()); }, t("workflow_node.deploy.form.tencentcloud_sslupdate_resource_regions.placeholder")), isReplaced: z.boolean().nullish(), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudSSLUpdate, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudVOD.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderTencentCloudVOD = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> } > } > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ endpoint: z.string().nullish(), subAppId: z .union([z.string(), z.number().int()]) .nullish() .refine((v) => { if (v == null) return true; return /^\d+$/.test(v + "") && +v > 0; }, t("workflow_node.deploy.form.tencentcloud_vod_sub_app_id.placeholder")), domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: { if (!isDomain(values.domain!)) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudVOD, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderTencentCloudWAF.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderTencentCloudWAF = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > } > } > } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", domain: "", domainId: "", instanceId: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ endpoint: z.string().nullish(), region: z.string().nonempty(t("workflow_node.deploy.form.tencentcloud_waf_region.placeholder")), domain: z.string().refine((v) => isDomain(v), t("common.errmsg.domain_invalid")), domainId: z.string().nonempty(t("workflow_node.deploy.form.tencentcloud_waf_domain_id.placeholder")), instanceId: z.string().nonempty(t("workflow_node.deploy.form.tencentcloud_waf_instance_id.placeholder")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderTencentCloudWAF, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderUCloudUALB.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; const RESOURCE_TYPE_LISTENER = "listener" as const; const BizDeployNodeConfigFieldsProviderUCloudUALB = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", resourceType: RESOURCE_TYPE_LISTENER, loadbalancerId: "", listenerId: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ endpoint: z.string().nullish(), resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t("workflow_node.deploy.form.shared_resource_type.placeholder")), region: z.string().nonempty(t("workflow_node.deploy.form.ucloud_ualb_region.placeholder")), loadbalancerId: z.string().nonempty(t("workflow_node.deploy.form.ucloud_ualb_loadbalancer_id.placeholder")), listenerId: z.string().nullish(), domain: z .string() .nullish() .refine((v) => { if (!v) return true; return isDomain(v); }, t("common.errmsg.domain_invalid")), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_LISTENER: { if (!values.listenerId?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.ucloud_ualb_listener_id.placeholder"), path: ["listenerId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderUCloudUALB, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderUCloudUCDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderUCloudUCDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { domainId: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ domainId: z.string().nonempty(t("workflow_node.deploy.form.ucloud_ucdn_domain_id.placeholder")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderUCloudUCDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderUCloudUCLB.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; const RESOURCE_TYPE_VSERVER = "vserver" as const; const BizDeployNodeConfigFieldsProviderUCloudUCLB = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", resourceType: RESOURCE_TYPE_VSERVER, loadbalancerId: "", vserverId: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ endpoint: z.string().nullish(), resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_VSERVER], t("workflow_node.deploy.form.shared_resource_type.placeholder")), region: z.string().nonempty(t("workflow_node.deploy.form.ucloud_uclb_region.placeholder")), loadbalancerId: z.string().nonempty(t("workflow_node.deploy.form.ucloud_uclb_loadbalancer_id.placeholder")), vserverId: z.string().nullish(), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_VSERVER: { if (!values.vserverId?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.ucloud_uclb_vserver_id.placeholder"), path: ["vserverId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderUCloudUCLB, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderUCloudUEWAF.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderUCloudUEWAF = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> ); }; const getInitialValues = (): Nullish>> => { return { domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ domain: z.string().refine((v) => isDomain(v), t("common.errmsg.domain_invalid")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderUCloudUEWAF, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderUCloudUPathX.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { isPortNumber } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderUCloudUPathX = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { acceleratorId: "", listenerPort: 443, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ acceleratorId: z.string().nonempty(t("workflow_node.deploy.form.ucloud_upathx_accelerator_id.placeholder")), listenerPort: z.coerce.number().refine((v) => isPortNumber(v), t("common.errmsg.port_invalid")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderUCloudUPathX, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderUCloudUS3.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderUCloudUS3 = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", bucket: "", domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ region: z.string().nonempty(t("workflow_node.deploy.form.ucloud_us3_region.placeholder")), bucket: z.string().nonempty(t("workflow_node.deploy.form.ucloud_us3_bucket.placeholder")), domain: z.string().refine((v) => isDomain(v), t("common.errmsg.domain_invalid")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderUCloudUS3, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderUniCloudWebHost.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Tips from "@/components/Tips"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderUniCloudWebHost = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } /> ); }; const getInitialValues = (): Nullish>> => { return { spaceProvider: "tencent", spaceId: "", domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ spaceProvider: z.string().nonempty(t("workflow_node.deploy.form.unicloud_webhost_space_provider.placeholder")), spaceId: z.string().nonempty(t("workflow_node.deploy.form.unicloud_webhost_space_id.placeholder")), domain: z.string().refine((v) => isDomain(v), t("common.errmsg.domain_invalid")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderUniCloudWebHost, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderUpyunCDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import Tips from "@/components/Tips"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderUpyunCDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> } /> ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderUpyunCDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderUpyunFile.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Tips from "@/components/Tips"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderUpyunFile = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } /> ); }; const getInitialValues = (): Nullish>> => { return { bucket: "", domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ bucket: z.string().nonempty(t("workflow_node.deploy.form.upyun_file_bucket.placeholder")), domain: z.string().refine((v) => isDomain(v), t("common.errmsg.domain_invalid")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderUpyunFile, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineALB.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; const RESOURCE_TYPE_LISTENER = "listener" as const; const BizDeployNodeConfigFieldsProviderVolcEngineALB = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", resourceType: RESOURCE_TYPE_LISTENER, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ region: z.string().nonempty(t("workflow_node.deploy.form.volcengine_alb_region.placeholder")), resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t("workflow_node.deploy.form.shared_resource_type.placeholder")), loadbalancerId: z.string().nullish(), listenerId: z.string().nullish(), domain: z .string() .nullish() .refine((v) => { if (!v) return true; return isDomain(v, { allowWildcard: true }); }, t("common.errmsg.domain_invalid")), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_LOADBALANCER: { if (!values.loadbalancerId?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.volcengine_alb_loadbalancer_id.placeholder"), path: ["loadbalancerId"], }); } } break; case RESOURCE_TYPE_LISTENER: { if (!values.listenerId?.trim()) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.volcengine_alb_listener_id.placeholder"), path: ["listenerId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderVolcEngineALB, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineCDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderVolcEngineCDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderVolcEngineCDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineCLB.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { useFormNestedFieldsContext } from "./_context"; const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; const RESOURCE_TYPE_LISTENER = "listener" as const; const BizDeployNodeConfigFieldsProviderVolcEngineCLB = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", resourceType: RESOURCE_TYPE_LISTENER, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ region: z.string().nonempty(t("workflow_node.deploy.form.volcengine_clb_region.placeholder")), resourceType: z.literal([RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER], t("workflow_node.deploy.form.shared_resource_type.placeholder")), loadbalancerId: z.string().nullish(), listenerId: z.string().nullish(), }) .superRefine((values, ctx) => { switch (values.resourceType) { case RESOURCE_TYPE_LOADBALANCER: { const scLoadbalancerId = z.string().nonempty(); if (!scLoadbalancerId.safeParse(values.loadbalancerId).success) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.volcengine_clb_loadbalancer_id.placeholder"), path: ["loadbalancerId"], }); } } break; case RESOURCE_TYPE_LISTENER: { const scListenerId = z.string().nonempty(); if (!scListenerId.safeParse(values.listenerId).success) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.volcengine_clb_listener_id.placeholder"), path: ["listenerId"], }); } } break; } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderVolcEngineCLB, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineCertCenter.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderVolcEngineCertCenter = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> ); }; const getInitialValues = (): Nullish>> => { return { region: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ region: z.string().nonempty(t("workflow_node.deploy.form.volcengine_certcenter_region.placeholder")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderVolcEngineCertCenter, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineDCDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderVolcEngineDCDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderVolcEngineDCDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineImageX.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderVolcEngineImageX = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", serviceId: "", domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ region: z.string().nonempty(t("workflow_node.deploy.form.volcengine_imagex_region.placeholder")), serviceId: z.string().nonempty(t("workflow_node.deploy.form.volcengine_imagex_service_id.placeholder")), domain: z.string().refine((v) => isDomain(v), t("common.errmsg.domain_invalid")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderVolcEngineImageX, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineLive.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const BizDeployNodeConfigFieldsProviderVolcEngineLive = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderVolcEngineLive, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineTOS.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderVolcEngineTOS = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", bucket: "", domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ region: z.string().nonempty(t("workflow_node.deploy.form.volcengine_tos_region.placeholder")), bucket: z.string().nonempty(t("workflow_node.deploy.form.volcengine_tos_bucket.placeholder")), domain: z.string().refine((v) => isDomain(v), t("common.errmsg.domain_invalid")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderVolcEngineTOS, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineVOD.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import Show from "@/components/Show"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const; const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const; const DOMAIN_TYPE_PLAY = "play" as const; const DOMAIN_TYPE_IMAGE = "image" as const; const BizDeployNodeConfigFieldsProviderVolcEngineVOD = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true, }); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { spaceName: "", domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domainType: DOMAIN_TYPE_PLAY, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ spaceName: z.string().nonempty(t("workflow_node.deploy.form.volcengine_vod_space_name.placeholder")).nullish(), domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domainType: z.literal([DOMAIN_TYPE_PLAY, DOMAIN_TYPE_IMAGE], t("workflow_node.deploy.form.volcengine_vod_domain_type.placeholder")), domain: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: case DOMAIN_MATCH_PATTERN_WILDCARD: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderVolcEngineVOD, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderVolcEngineWAF.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const ACCESS_MODE_CNAME = "cname" as const; const BizDeployNodeConfigFieldsProviderVolcEngineWAF = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return { region: "", accessMode: ACCESS_MODE_CNAME, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ region: z.string().nonempty(t("workflow_node.deploy.form.volcengine_waf_region.placeholder")), accessMode: z.literal([ACCESS_MODE_CNAME], t("workflow_node.deploy.form.volcengine_waf_access_mode.placeholder")), domain: z.string().refine((v) => isDomain(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderVolcEngineWAF, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderWangsuCDN.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import MultipleSplitValueInput from "@/components/MultipleSplitValueInput"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const MULTIPLE_INPUT_SEPARATOR = ";"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const BizDeployNodeConfigFieldsProviderWangsuCDN = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> ); }; const getInitialValues = (): Nullish>> => { return { domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domains: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domains: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: { const valid = values.domains && values.domains.split(MULTIPLE_INPUT_SEPARATOR).every((e) => isDomain(e, { allowWildcard: true })); if (!valid) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domains"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderWangsuCDN, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderWangsuCDNPro.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Radio, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { isDomain } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const; const BizDeployNodeConfigFieldsProviderWangsuCDNPro = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true }); return ( <> ) : ( void 0 ) } rules={[formRule]} > ({ key: s, label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`), value: s, }))} /> } > } > ); }; const getInitialValues = (): Nullish>> => { return { environment: "production", domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT, domain: "", }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ environment: z.literal(["production", "staging"], t("workflow_node.deploy.form.wangsu_cdnpro_environment.placeholder")), domainMatchPattern: z.string().nonempty(t("workflow_node.deploy.form.shared_domain_match_pattern.placeholder")).default(DOMAIN_MATCH_PATTERN_EXACT), domain: z.string().nullish(), certificateId: z.string().nullish(), webhookId: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.domainMatchPattern) { switch (values.domainMatchPattern) { case DOMAIN_MATCH_PATTERN_EXACT: { if (!isDomain(values.domain!, { allowWildcard: true })) { ctx.addIssue({ code: "custom", message: t("common.errmsg.domain_invalid"), path: ["domain"], }); } } break; } } }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderWangsuCDNPro, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderWangsuCertificate.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderWangsuCertificate = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> } > ); }; const getInitialValues = (): Nullish>> => { return {}; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t: _ } = i18n; return z.object({ certificateId: z.string().nullish(), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderWangsuCertificate, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderWebhook.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { IconBulb } from "@tabler/icons-react"; import { Button, Form, Input, Popover } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import CodeTextInput from "@/components/CodeTextInput"; import { isJsonObject } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderWebhook = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const handleWebhookDataBlur = () => { const value = formInst.getFieldValue([parentNamePath, "webhookData"]); try { const json = JSON.stringify(JSON.parse(value), null, 2); formInst.setFieldValue([parentNamePath, "webhookData"], json); } catch { return; } }; return ( <>
} mouseEnterDelay={1}>
); }; const getInitialValues = (): Nullish>> => { return {}; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ webhookData: z .string() .nullish() .refine((v) => { if (!v) return true; return isJsonObject(v); }, t("common.errmsg.json_invalid")), timeout: z.preprocess( (v) => (v == null || v === "" ? void 0 : Number(v)), z.number().int().gte(1, t("workflow_node.deploy.form.webhook_timeout.placeholder")).nullish() ), }); }; const _default = Object.assign(BizDeployNodeConfigFieldsProviderWebhook, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizDeployNodeConfigForm.tsx ================================================ import { useEffect, useMemo } from "react"; import { getI18n, useTranslation } from "react-i18next"; import { type FlowNodeEntity } from "@flowgram.ai/fixed-layout-editor"; import { IconPlus } from "@tabler/icons-react"; import { type AnchorProps, Button, Divider, Form, type FormInstance, Select, Switch, Typography, theme } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import AccessEditDrawer from "@/components/access/AccessEditDrawer"; import AccessSelect from "@/components/access/AccessSelect"; import DeploymentProviderPicker from "@/components/provider/DeploymentProviderPicker"; import DeploymentProviderSelect from "@/components/provider/DeploymentProviderSelect"; import Show from "@/components/Show"; import { type AccessModel } from "@/domain/access"; import { deploymentProvidersMap } from "@/domain/provider"; import { type WorkflowNodeConfigForBizDeploy, defaultNodeConfigForBizDeploy } from "@/domain/workflow"; import { useAntdForm, useZustandShallowSelector } from "@/hooks"; import { useAccessesStore } from "@/stores/access"; import { getAllPreviousNodes } from "../_util"; import { FormNestedFieldsContextProvider, NodeFormContextProvider } from "./_context"; import BizDeployNodeConfigFieldsProvider from "./BizDeployNodeConfigFieldsProvider"; import { NodeType } from "../nodes/typings"; export interface BizDeployNodeConfigFormProps { form: FormInstance; node: FlowNodeEntity; } const BizDeployNodeConfigForm = ({ node, ...props }: BizDeployNodeConfigFormProps) => { if (node.flowNodeType !== NodeType.BizDeploy) { console.warn(`[certimate] current workflow node type is not: ${NodeType.BizDeploy}`); } const { i18n, t } = useTranslation(); const { token: themeToken } = theme.useToken(); const { accesses } = useAccessesStore(useZustandShallowSelector("accesses")); const accessOptionFilter = (_: string, option: AccessModel) => { if (option.reserve) return false; return deploymentProvidersMap.get(fieldProvider)?.provider === option.provider; }; const initialValues = useMemo(() => { return node.form?.getValueIn("config") as WorkflowNodeConfigForBizDeploy | undefined; }, [node]); const formSchema = getSchema({ i18n }).superRefine((values, ctx) => { if (values.certificateOutputNodeId) { if (!certificateOutputNodeIdOptions.some((option) => option.value === values.certificateOutputNodeId)) { ctx.addIssue({ code: "custom", message: t("workflow_node.deploy.form.certificate_output_node_id.placeholder"), path: ["certificateOutputNodeId"], }); } } }); const formRule = createSchemaFieldRule(formSchema); const { form: formInst, formProps } = useAntdForm>({ form: props.form, name: "workflowNodeBizDeployConfigForm", initialValues: initialValues ?? getInitialValues(), }); const fieldProvider = Form.useWatch("provider", { form: formInst, preserve: true }); const fieldProviderAccessId = Form.useWatch("providerAccessId", { form: formInst, preserve: true }); const certificateOutputNodeIdOptions = useMemo(() => { return getAllPreviousNodes(node) .filter((node) => node.flowNodeType === NodeType.BizApply || node.flowNodeType === NodeType.BizUpload) .map((node) => { return { label: node.form?.getValueIn("name"), value: node.id, }; }); }, [node]); const renderNestedFieldProviderComponent = BizDeployNodeConfigFieldsProvider.useComponent(fieldProvider, {}); const showProviderAccess = useMemo(() => { // 内置的部署提供商(如本地部署)无需显示授权信息字段 if (fieldProvider) { const provider = deploymentProvidersMap.get(fieldProvider); return !provider?.builtin; } return false; }, [fieldProvider]); useEffect(() => { // 如果未选择部署目标,则清空授权信息 if (!fieldProvider && fieldProviderAccessId) { formInst.setFieldValue("providerAccessId", void 0); return; } // 如果已选择部署目标只有一个授权信息,则自动选择该授权信息 if (fieldProvider && !fieldProviderAccessId) { const availableAccesses = accesses .filter((access) => accessOptionFilter(access.provider, access)) .filter((access) => deploymentProvidersMap.get(fieldProvider)?.provider === access.provider); if (availableAccesses.length === 1) { formInst.setFieldValue("providerAccessId", availableAccesses[0].id); } } }, [fieldProvider, fieldProviderAccessId]); const handleProviderPick = (value: string) => { formInst.setFieldValue("provider", value); formInst.setFieldValue("providerAccessId", void 0); formInst.setFieldValue("providerConfig", void 0); }; const handleProviderSelect = (value?: string | undefined) => { // 切换部署目标时重置表单,避免其他部署目标的配置字段影响当前部署目标 if (initialValues?.provider === value) { formInst.setFieldValue("providerAccessId", void 0); formInst.resetFields(["providerConfig"]); } else { formInst.setFieldValue("providerAccessId", void 0); formInst.setFieldValue("providerConfig", void 0); } }; return (
); }; const getAnchorItems = ({ i18n = getI18n() }: { i18n?: ReturnType }): Required["items"] => { const { t } = i18n; return ["parameters"].map((key) => ({ key: key, title: t(`workflow_node.monitor.form_anchor.${key}.tab`), href: "#" + key, })); }; const getInitialValues = (): Nullish>> => { return defaultNodeConfigForBizMonitor(); }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ host: z.string().refine((v) => isHostname(v), t("common.errmsg.host_invalid")), port: z.coerce.number().refine((v) => isPortNumber(v), t("common.errmsg.port_invalid")), domain: z .string() .nullish() .refine((v) => { if (!v) return true; return isDomain(v); }, t("common.errmsg.domain_invalid")), requestPath: z.string().nullish(), }); }; const _default = Object.assign(BizMonitorNodeConfigForm, { getAnchorItems, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizNotifyNodeConfigDrawer.tsx ================================================ import { useTranslation } from "react-i18next"; import { type FlowNodeEntity } from "@flowgram.ai/fixed-layout-editor"; import { Form } from "antd"; import { type WorkflowNodeConfigForBizNotify } from "@/domain/workflow"; import { NodeConfigDrawer } from "./_shared"; import BizNotifyNodeConfigForm from "./BizNotifyNodeConfigForm"; import { NodeType } from "../nodes/typings"; export interface BizNotifyNodeConfigDrawerProps { afterClose?: () => void; loading?: boolean; node: FlowNodeEntity; open?: boolean; onOpenChange?: (open: boolean) => void; } const BizNotifyNodeConfigDrawer = ({ node, ...props }: BizNotifyNodeConfigDrawerProps) => { if (node.flowNodeType !== NodeType.BizNotify) { console.warn(`[certimate] current workflow node type is not: ${NodeType.BizNotify}`); } const { i18n } = useTranslation(); const [formInst] = Form.useForm(); const fieldProvider = Form.useWatch("provider", { form: formInst, preserve: true }); return ( ); }; export default BizNotifyNodeConfigDrawer; ================================================ FILE: ui/src/components/workflow/designer/forms/BizNotifyNodeConfigFieldsProvider.tsx ================================================ import { useEffect, useState } from "react"; import { NOTIFICATION_PROVIDERS, type NotificationProviderType } from "@/domain/provider"; import BizNotifyNodeConfigFieldsProviderDiscordBot from "./BizNotifyNodeConfigFieldsProviderDiscordBot"; import BizNotifyNodeConfigFieldsProviderEmail from "./BizNotifyNodeConfigFieldsProviderEmail"; import BizNotifyNodeConfigFieldsProviderMattermost from "./BizNotifyNodeConfigFieldsProviderMattermost"; import BizNotifyNodeConfigFieldsProviderSlackBot from "./BizNotifyNodeConfigFieldsProviderSlackBot"; import BizNotifyNodeConfigFieldsProviderTelegramBot from "./BizNotifyNodeConfigFieldsProviderTelegramBot"; import BizNotifyNodeConfigFieldsProviderWebhook from "./BizNotifyNodeConfigFieldsProviderWebhook"; const providerComponentMap: Partial>> = { /* 注意:如果追加新的子组件,请保持以 ASCII 排序。 NOTICE: If you add new child component, please keep ASCII order. */ [NOTIFICATION_PROVIDERS.DISCORDBOT]: BizNotifyNodeConfigFieldsProviderDiscordBot, [NOTIFICATION_PROVIDERS.EMAIL]: BizNotifyNodeConfigFieldsProviderEmail, [NOTIFICATION_PROVIDERS.MATTERMOST]: BizNotifyNodeConfigFieldsProviderMattermost, [NOTIFICATION_PROVIDERS.SLACKBOT]: BizNotifyNodeConfigFieldsProviderSlackBot, [NOTIFICATION_PROVIDERS.TELEGRAMBOT]: BizNotifyNodeConfigFieldsProviderTelegramBot, [NOTIFICATION_PROVIDERS.WEBHOOK]: BizNotifyNodeConfigFieldsProviderWebhook, }; const useComponent = (provider: string, { initProps, deps = [] }: { initProps?: (provider: string) => any; deps?: unknown[] }) => { const initComponent = () => { const Component = providerComponentMap[provider as NotificationProviderType]; if (!Component) return null; const props = initProps?.(provider); if (props) { return ; } return ; }; const [component, setComponent] = useState(() => initComponent()); useEffect(() => setComponent(initComponent()), [provider]); useEffect(() => setComponent(initComponent()), deps); return component; }; const _default = { useComponent, }; export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizNotifyNodeConfigFieldsProviderDiscordBot.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizNotifyNodeConfigFieldsProviderDiscordBot = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> ); }; const getInitialValues = (): Nullish>> => { return {}; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t: _ } = i18n; return z.object({ channelId: z.string().nullish(), }); }; const _default = Object.assign(BizNotifyNodeConfigFieldsProviderDiscordBot, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizNotifyNodeConfigFieldsProviderEmail.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { isEmail } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const MESSAGE_FORMAT_PLAIN = "plain" as const; const MESSAGE_FORMAT_HTML = "html" as const; const BizNotifyNodeConfigFieldsProviderEmail = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> ); }; const getInitialValues = (): Nullish>> => { return { format: MESSAGE_FORMAT_PLAIN, }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ format: z.enum([MESSAGE_FORMAT_PLAIN, MESSAGE_FORMAT_HTML]).nullish(), receiverAddress: z .string() .nullish() .refine((v) => { if (!v) return true; return isEmail(v); }, t("common.errmsg.email_invalid")), }); }; const _default = Object.assign(BizNotifyNodeConfigFieldsProviderEmail, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizNotifyNodeConfigFieldsProviderMattermost.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizNotifyNodeConfigFieldsProviderMattermost = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> ); }; const getInitialValues = (): Nullish>> => { return {}; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t: _ } = i18n; return z.object({ channelId: z.string().nullish(), }); }; const _default = Object.assign(BizNotifyNodeConfigFieldsProviderMattermost, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizNotifyNodeConfigFieldsProviderSlackBot.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizNotifyNodeConfigFieldsProviderSlackBot = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> ); }; const getInitialValues = (): Nullish>> => { return {}; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t: _ } = i18n; return z.object({ channelId: z.string().nullish(), }); }; const _default = Object.assign(BizNotifyNodeConfigFieldsProviderSlackBot, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizNotifyNodeConfigFieldsProviderTelegramBot.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useFormNestedFieldsContext } from "./_context"; const BizNotifyNodeConfigFieldsProviderTelegramBot = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const initialValues = getInitialValues(); return ( <> ); }; const getInitialValues = (): Nullish>> => { return {}; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ chatId: z .preprocess( (v) => (v == null || v === "" ? void 0 : Number(v)), z .number() .nullish() .refine((v) => { if (v == null || v + "" === "") return true; return !Number.isNaN(+v!) && +v! !== 0; }, t("workflow_node.notify.form.telegrambot_chat_id.placeholder")) ) .nullish(), }); }; const _default = Object.assign(BizNotifyNodeConfigFieldsProviderTelegramBot, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizNotifyNodeConfigFieldsProviderWebhook.tsx ================================================ import { getI18n, useTranslation } from "react-i18next"; import { IconBulb } from "@tabler/icons-react"; import { Button, Form, Input, Popover } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import CodeTextInput from "@/components/CodeTextInput"; import { isJsonObject } from "@/utils/validator"; import { useFormNestedFieldsContext } from "./_context"; const BizNotifyNodeConfigFieldsProviderWebhook = () => { const { i18n, t } = useTranslation(); const { parentNamePath } = useFormNestedFieldsContext(); const formSchema = z.object({ [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); const handleWebhookDataBlur = () => { const value = formInst.getFieldValue([parentNamePath, "webhookData"]); try { const json = JSON.stringify(JSON.parse(value), null, 2); formInst.setFieldValue([parentNamePath, "webhookData"], json); } catch { return; } }; return ( <>
} mouseEnterDelay={1}>
); }; const getInitialValues = (): Nullish>> => { return {}; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ webhookData: z .string() .nullish() .refine((v) => { if (!v) return true; return isJsonObject(v); }, t("common.errmsg.json_invalid")), timeout: z.preprocess( (v) => (v == null || v === "" ? void 0 : Number(v)), z.number().int().gte(1, t("workflow_node.notify.form.webhook_timeout.placeholder")).nullish() ), }); }; const _default = Object.assign(BizNotifyNodeConfigFieldsProviderWebhook, { getInitialValues, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizNotifyNodeConfigForm.tsx ================================================ import { useEffect, useMemo } from "react"; import { getI18n, useTranslation } from "react-i18next"; import { type FlowNodeEntity } from "@flowgram.ai/fixed-layout-editor"; import { IconChevronDown, IconPlus } from "@tabler/icons-react"; import { type AnchorProps, Button, Divider, Form, type FormInstance, Input, Switch, Typography } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import AccessEditDrawer from "@/components/access/AccessEditDrawer"; import AccessSelect from "@/components/access/AccessSelect"; import PresetNotifyTemplatesPopselect from "@/components/preset/PresetNotifyTemplatesPopselect"; import NotificationProviderPicker from "@/components/provider/NotificationProviderPicker"; import NotificationProviderSelect from "@/components/provider/NotificationProviderSelect"; import Show from "@/components/Show"; import Tips from "@/components/Tips"; import { type AccessModel } from "@/domain/access"; import { notificationProvidersMap } from "@/domain/provider"; import { type WorkflowNodeConfigForBizNotify, defaultNodeConfigForBizNotify } from "@/domain/workflow"; import { useAntdForm, useZustandShallowSelector } from "@/hooks"; import { useAccessesStore } from "@/stores/access"; import { FormNestedFieldsContextProvider, NodeFormContextProvider } from "./_context"; import BizNotifyNodeConfigFieldsProvider from "./BizNotifyNodeConfigFieldsProvider"; import { NodeType } from "../nodes/typings"; export interface BizNotifyNodeConfigFormProps { form: FormInstance; node: FlowNodeEntity; } const BizNotifyNodeConfigForm = ({ node, ...props }: BizNotifyNodeConfigFormProps) => { if (node.flowNodeType !== NodeType.BizNotify) { console.warn(`[certimate] current workflow node type is not: ${NodeType.BizNotify}`); } const { i18n, t } = useTranslation(); const { accesses } = useAccessesStore(useZustandShallowSelector("accesses")); const accessOptionFilter = (_: string, option: AccessModel) => { if (option.reserve !== "notif") return false; return notificationProvidersMap.get(fieldProvider)?.provider === option.provider; }; const initialValues = useMemo(() => { return node.form?.getValueIn("config") as WorkflowNodeConfigForBizNotify | undefined; }, [node]); const formSchema = getSchema({ i18n }); const formRule = createSchemaFieldRule(formSchema); const { form: formInst, formProps } = useAntdForm>({ form: props.form, name: "workflowNodeBizNotifyNodeConfigForm", initialValues: initialValues ?? getInitialValues(), }); const fieldProvider = Form.useWatch("provider", { form: formInst, preserve: true }); const fieldProviderAccessId = Form.useWatch("providerAccessId", { form: formInst, preserve: true }); const renderNestedFieldProviderComponent = BizNotifyNodeConfigFieldsProvider.useComponent(fieldProvider, {}); useEffect(() => { // 如果未选择通知渠道,则清空授权信息 if (!fieldProvider && fieldProviderAccessId) { formInst.setFieldValue("providerAccessId", void 0); return; } // 如果已选择通知渠道只有一个授权信息,则自动选择该授权信息 if (fieldProvider && !fieldProviderAccessId) { const availableAccesses = accesses .filter((access) => accessOptionFilter(access.provider, access)) .filter((access) => notificationProvidersMap.get(fieldProvider)?.provider === access.provider); if (availableAccesses.length === 1) { formInst.setFieldValue("providerAccessId", availableAccesses[0].id); } } }, [fieldProvider, fieldProviderAccessId]); const handleProviderPick = (value: string) => { formInst.setFieldValue("provider", value); formInst.setFieldValue("providerAccessId", void 0); formInst.setFieldValue("providerConfig", void 0); }; const handleProviderSelect = (value?: string | undefined) => { // 切换通知渠道时重置表单,避免其他通知渠道的配置字段影响当前通知渠道 if (initialValues?.provider === value) { formInst.setFieldValue("providerAccessId", void 0); formInst.resetFields(["providerConfig"]); } else { formInst.setFieldValue("providerAccessId", void 0); formInst.setFieldValue("providerConfig", void 0); } }; return (
{ if (template) { formInst.setFieldValue("subject", template.subject); formInst.setFieldValue("message", template.message); } }} >
} />
{t("workflow_node.notify.form_anchor.channel.title")}
{t("workflow_node.notify.form.provider_access.button")} } usage="notification" afterSubmit={(record) => { if (!accessOptionFilter(record.provider, record)) return; if (notificationProvidersMap.get(fieldProvider!)?.provider !== record.provider) return; formInst.setFieldValue("providerAccessId", record.id); }} />
{renderNestedFieldProviderComponent && <>{renderNestedFieldProviderComponent}}
{t("workflow_node.notify.form_anchor.strategy.title")} {t("workflow_node.notify.form.skip_on_all_prev_skipped.prefix")} {t("workflow_node.notify.form.skip_on_all_prev_skipped.suffix")}
); }; const getAnchorItems = ({ i18n = getI18n() }: { i18n?: ReturnType }): Required["items"] => { const { t } = i18n; return ["parameters", "channel", "strategy"].map((key) => ({ key: key, title: t(`workflow_node.notify.form_anchor.${key}.tab`), href: "#" + key, })); }; const getInitialValues = (): Nullish>> => { return { subject: "", message: "", ...(defaultNodeConfigForBizNotify() as Nullish>>), }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z.object({ subject: z.string().nonempty(t("workflow_node.notify.form.subject.placeholder")), message: z.string().nonempty(t("workflow_node.notify.form.message.placeholder")), provider: z.string().nonempty(t("workflow_node.notify.form.provider.placeholder")), providerAccessId: z.string().nonempty(t("workflow_node.notify.form.provider_access.placeholder")), providerConfig: z.any().nullish(), skipOnAllPrevSkipped: z.boolean().nullish(), }); }; const _default = Object.assign(BizNotifyNodeConfigForm, { getAnchorItems, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BizUploadNodeConfigDrawer.tsx ================================================ import { useTranslation } from "react-i18next"; import { type FlowNodeEntity } from "@flowgram.ai/fixed-layout-editor"; import { Form } from "antd"; import { NodeConfigDrawer } from "./_shared"; import BizUploadNodeConfigForm from "./BizUploadNodeConfigForm"; import { NodeType } from "../nodes/typings"; export interface BizUploadNodeConfigDrawerProps { afterClose?: () => void; loading?: boolean; node: FlowNodeEntity; open?: boolean; onOpenChange?: (open: boolean) => void; } const BizUploadNodeConfigDrawer = ({ node, ...props }: BizUploadNodeConfigDrawerProps) => { if (node.flowNodeType !== NodeType.BizUpload) { console.warn(`[certimate] current workflow node type is not: ${NodeType.BizUpload}`); } const { i18n } = useTranslation(); const [formInst] = Form.useForm(); return ( ); }; export default BizUploadNodeConfigDrawer; ================================================ FILE: ui/src/components/workflow/designer/forms/BizUploadNodeConfigForm.tsx ================================================ import { useMemo } from "react"; import { getI18n, useTranslation } from "react-i18next"; import { type FlowNodeEntity } from "@flowgram.ai/fixed-layout-editor"; import { type AnchorProps, Form, type FormInstance, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import FileTextInput from "@/components/FileTextInput"; import Show from "@/components/Show"; import Tips from "@/components/Tips"; import { type WorkflowNodeConfigForBizUpload, defaultNodeConfigForBizUpload } from "@/domain/workflow"; import { useAntdForm } from "@/hooks"; import { isUrlWithHttpOrHttps } from "@/utils/validator"; import { getCertificateSubjectAltNames as getX509SubjectAltNames, validatePEMCertificate, validatePEMPrivateKey } from "@/utils/x509"; import { NodeFormContextProvider } from "./_context"; import { NodeType } from "../nodes/typings"; export interface BizUploadNodeConfigFormProps { form: FormInstance; node: FlowNodeEntity; } const UPLOAD_SOURCE_FORM = "form" as const; const UPLOAD_SOURCE_LOCAL = "local" as const; const UPLOAD_SOURCE_URL = "url" as const; const BizUploadNodeConfigForm = ({ node, ...props }: BizUploadNodeConfigFormProps) => { if (node.flowNodeType !== NodeType.BizUpload) { console.warn(`[certimate] current workflow node type is not: ${NodeType.BizUpload}`); } const { i18n, t } = useTranslation(); const initialValues = useMemo(() => { return node.form?.getValueIn("config") as WorkflowNodeConfigForBizUpload | undefined; }, [node]); const formSchema = getSchema({ i18n }); const formRule = createSchemaFieldRule(formSchema); const { form: formInst, formProps } = useAntdForm>({ form: props.form, name: "workflowNodeBizUploadConfigForm", initialValues: initialValues ?? getInitialValues(), }); const fieldSource = Form.useWatch("source", { form: formInst, preserve: true }); const fieldCertificate = Form.useWatch("certificate", { form: formInst, preserve: true }); const fieldName = useMemo(() => { if (!fieldSource || fieldSource === UPLOAD_SOURCE_FORM) { return fieldCertificate ? getX509SubjectAltNames(fieldCertificate).join(";") : void 0; } return void 0; }, [fieldSource, fieldCertificate]); const handleSourceChange = (value: string) => { if (value === initialValues?.source) { formInst.resetFields(["certificate", "privateKey"]); } else { setTimeout(() => { formInst.setFieldValue("certificate", ""); formInst.setFieldValue("privateKey", ""); }, 0); } }; return (
handleSourceChange(e.target.value)}> {t("workflow_node.upload.form.source.option.form.label")} {t("workflow_node.upload.form.source.option.local.label")} {t("workflow_node.upload.form.source.option.url.label")}
); }; const getAnchorItems = ({ i18n = getI18n() }: { i18n?: ReturnType }): Required["items"] => { const { t } = i18n; return ["parameters"].map((key) => ({ key: key, title: t(`workflow_node.upload.form_anchor.${key}.tab`), href: "#" + key, })); }; const getInitialValues = (): Nullish>> => { return { certificate: "", privateKey: "", ...(defaultNodeConfigForBizUpload() as Nullish>>), }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ source: z.enum([UPLOAD_SOURCE_FORM, UPLOAD_SOURCE_LOCAL, UPLOAD_SOURCE_URL], t("workflow_node.upload.form.source.placeholder")), name: z.string().nullish(), certificate: z.string().nonempty(), privateKey: z.string().nonempty(), }) .superRefine((values, ctx) => { switch (values.source) { case UPLOAD_SOURCE_FORM: { if (!validatePEMCertificate(values.certificate)) { ctx.addIssue({ code: "custom", message: t("workflow_node.upload.form.certificate_pem.errmsg.invalid"), path: ["certificate"], }); } if (!validatePEMPrivateKey(values.privateKey)) { ctx.addIssue({ code: "custom", message: t("workflow_node.upload.form.private_key_pem.errmsg.invalid"), path: ["privateKey"], }); } } break; case UPLOAD_SOURCE_LOCAL: { if (!z.string().nonempty().safeParse(values.certificate).success) { ctx.addIssue({ code: "custom", message: t("workflow_node.upload.form.certificate_path.placeholder"), path: ["certificate"], }); } if (!z.string().nonempty().safeParse(values.privateKey).success) { ctx.addIssue({ code: "custom", message: t("workflow_node.upload.form.private_key_path.placeholder"), path: ["privateKey"], }); } } break; case UPLOAD_SOURCE_URL: { if (!isUrlWithHttpOrHttps(values.certificate)) { ctx.addIssue({ code: "custom", message: t("workflow_node.upload.form.certificate_url.placeholder"), path: ["certificate"], }); } if (!isUrlWithHttpOrHttps(values.privateKey)) { ctx.addIssue({ code: "custom", message: t("workflow_node.upload.form.private_key_url.placeholder"), path: ["privateKey"], }); } } break; default: { ctx.addIssue({ code: "custom", message: t("workflow_node.upload.form.source.placeholder"), path: ["source"], }); } break; } }); }; const _default = Object.assign(BizUploadNodeConfigForm, { getAnchorItems, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/BranchBlockNodeConfigDrawer.tsx ================================================ import { useTranslation } from "react-i18next"; import { type FlowNodeEntity } from "@flowgram.ai/fixed-layout-editor"; import { Form } from "antd"; import { NodeConfigDrawer } from "./_shared"; import BranchBlockNodeConfigForm from "./BranchBlockNodeConfigForm"; import { NodeType } from "../nodes/typings"; export interface BranchBlockNodeConfigDrawerProps { afterClose?: () => void; loading?: boolean; node: FlowNodeEntity; open?: boolean; onOpenChange?: (open: boolean) => void; } const BranchBlockNodeConfigDrawer = ({ node, ...props }: BranchBlockNodeConfigDrawerProps) => { if (node.flowNodeType !== NodeType.BranchBlock) { console.warn(`[certimate] current workflow node type is not: ${NodeType.BranchBlock}`); } const { i18n } = useTranslation(); const [formInst] = Form.useForm(); return ( ); }; export default BranchBlockNodeConfigDrawer; ================================================ FILE: ui/src/components/workflow/designer/forms/BranchBlockNodeConfigExprInputBox.tsx ================================================ import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { IconCircleMinus, IconCirclePlus } from "@tabler/icons-react"; import { useControllableValue } from "ahooks"; import { Button, Form, Input, Radio, Select, theme } from "antd"; import Show from "@/components/Show"; import { type Expr, type ExprComparisonOperator, type ExprLogicalOperator, ExprType, type ExprValue, type ExprValueSelector, type ExprValueType, } from "@/domain/workflow"; import { useAntdFormName } from "@/hooks"; import { useNodeFormContext } from "./_context"; import { getAllPreviousNodes } from "../_util"; import { NodeType } from "../nodes/typings"; export interface BranchBlockNodeConfigExprInputBoxProps { className?: string; style?: React.CSSProperties; defaultValue?: Expr; value?: Expr; onChange?: (value: Expr) => void; } export interface BranchBlockNodeConfigExprInputBoxInstance { validate: () => Promise; } // 表单内部使用的扁平结构 type ConditionItem = { // 选择器,格式为 "${nodeId}#${outputName}#${valueType}" // 将 [ExprValueSelector] 转为字符串形式,以便于结构化存储。 leftSelector?: string; // 比较运算符。 operator?: ExprComparisonOperator; // 值。 // 将 [ExprValue] 转为字符串形式,以便于结构化存储。 rightValue?: string; }; type ConditionFormValues = { conditions: ConditionItem[]; logicalOperator: ExprLogicalOperator; }; const exprToFormValues = (expr: Expr | undefined): ConditionFormValues => { if (!expr) return getInitialValues(); const conditions: ConditionItem[] = []; let logicalOp: ExprLogicalOperator = "and"; const extractExpr = (expr: Expr): void => { if (expr.type === ExprType.Comparison) { if (expr.left.type == ExprType.Variant && expr.right.type == ExprType.Constant) { conditions.push({ leftSelector: expr.left.selector?.id != null ? `${expr.left.selector.id}#${expr.left.selector.name}#${expr.left.selector.type}` : void 0, operator: expr.operator != null ? expr.operator : void 0, rightValue: expr.right?.value != null ? String(expr.right.value) : void 0, }); } else { console.warn("[certimate] invalid comparison expression: left must be a variant and right must be a constant", expr); } } else if (expr.type === ExprType.Logical) { logicalOp = expr.operator || "and"; extractExpr(expr.left); extractExpr(expr.right); } }; extractExpr(expr); return { conditions: conditions, logicalOperator: logicalOp, }; }; const formValuesToExpr = (values: ConditionFormValues): Expr | undefined => { const wrapExpr = (condition: ConditionItem): Expr => { const [id, name, type] = (condition.leftSelector?.split("#") ?? ["", "", ""]) as [string, string, ExprValueType]; const valid = !!id && !!name && !!type; const left: Expr = { type: ExprType.Variant, selector: valid ? { id: id, name: name, type: type, } : ({} as ExprValueSelector), }; const right: Expr = { type: ExprType.Constant, value: condition.rightValue!, valueType: type, }; return { type: ExprType.Comparison, operator: condition.operator!, left, right, }; }; if (values.conditions.length === 0) { return; } // 只有一个条件时,直接返回比较表达式 if (values.conditions.length === 1) { const { leftSelector, operator, rightValue } = values.conditions[0]; if (!leftSelector || !operator || !rightValue) { return; } return wrapExpr(values.conditions[0]); } // 多个条件时,通过逻辑运算符连接 let expr: Expr = wrapExpr(values.conditions[0]); for (let i = 1; i < values.conditions.length; i++) { expr = { type: ExprType.Logical, operator: values.logicalOperator, left: expr, right: wrapExpr(values.conditions[i]), }; } return expr; }; const BranchBlockNodeConfigExprInputBox = forwardRef( ({ className, style, ...props }, ref) => { const { t } = useTranslation(); const { token: themeToken } = theme.useToken(); const [value, setValue] = useControllableValue(props, { valuePropName: "value", defaultValuePropName: "defaultValue", trigger: "onChange", }); const { node } = useNodeFormContext(); const [formInst] = Form.useForm(); const formName = useAntdFormName({ form: formInst, name: "workflowNodeBranchBlockConfigExprInputBoxForm" }); const [formModel, setFormModel] = useState(getInitialValues()); useEffect(() => { if (value) { const formValues = exprToFormValues(value); formInst.setFieldsValue(formValues); setFormModel(formValues); } else { formInst.resetFields(); setFormModel(getInitialValues()); } }, [value]); const ciSelectorOptions = useMemo(() => { return getAllPreviousNodes(node) .filter((node) => node.flowNodeType === NodeType.BizApply || node.flowNodeType === NodeType.BizUpload || node.flowNodeType === NodeType.BizMonitor) .map((node) => { const form = node.form; const group = { data: { name: form?.getValueIn("name"), ...form?.values, }, label: (
{form?.getValueIn("name")}
(NodeID: {node.id})
), options: Array<{ label: string; value: string }>(), }; group.options.push({ label: `${t("workflow.variables.type.certificate.label")} - ${t("workflow.variables.selector.hours_left.label")}`, value: `${node.id}#certificate.hoursLeft#number`, }); group.options.push({ label: `${t("workflow.variables.type.certificate.label")} - ${t("workflow.variables.selector.days_left.label")}`, value: `${node.id}#certificate.daysLeft#number`, }); group.options.push({ label: `${t("workflow.variables.type.certificate.label")} - ${t("workflow.variables.selector.validity.label")}`, value: `${node.id}#certificate.validity#boolean`, }); return group; }) .filter((item) => item.options.length > 0); }, [node]); const getValueTypeBySelector = (selector: string): ExprValueType | undefined => { if (!selector) return; const parts = selector.split("#"); if (parts.length >= 3) { return parts[2].toLowerCase() as ExprValueType; } }; const getOperatorsBySelector = (selector: string): { value: ExprComparisonOperator; label: string }[] => { const valueType = getValueTypeBySelector(selector); return getOperatorsByValueType(valueType!); }; const getOperatorsByValueType = (valueType: ExprValue): { value: ExprComparisonOperator; label: string }[] => { switch (valueType) { case "number": return [ { value: "eq", label: t("workflow_node.branch_block.form.expression.operator.option.eq.label") }, { value: "neq", label: t("workflow_node.branch_block.form.expression.operator.option.neq.label") }, { value: "gt", label: t("workflow_node.branch_block.form.expression.operator.option.gt.label") }, { value: "gte", label: t("workflow_node.branch_block.form.expression.operator.option.gte.label") }, { value: "lt", label: t("workflow_node.branch_block.form.expression.operator.option.lt.label") }, { value: "lte", label: t("workflow_node.branch_block.form.expression.operator.option.lte.label") }, ]; case "string": return [ { value: "eq", label: t("workflow_node.branch_block.form.expression.operator.option.eq.label") }, { value: "neq", label: t("workflow_node.branch_block.form.expression.operator.option.neq.label") }, ]; case "boolean": return [ { value: "eq", label: t("workflow_node.branch_block.form.expression.operator.option.eq.alias_is_label") }, { value: "neq", label: t("workflow_node.branch_block.form.expression.operator.option.neq.alias_not_label") }, ]; default: return []; } }; const handleFormChange = (_: unknown, values: ConditionFormValues) => { // TODO: 这里直接用参数 `values` 会丢失部分字段,引发 Issue #1096。 // 暂时先用 `getFieldsValue()` 代替,待排查原因,疑似与 antd v6 升级有关。 setTimeout(() => { values = formInst.getFieldsValue(); const expr = formValuesToExpr(values); setValue(expr); }, 0); }; useImperativeHandle(ref, () => { return { validate: async () => { const formValues = await formInst.validateFields(); return formValuesToExpr(formValues); }, }; }); return (
1}> {t("workflow_node.branch_block.form.expression.logical_operator.option.and.label")} {t("workflow_node.branch_block.form.expression.logical_operator.option.or.label")} {(fields, { add, remove }) => (
{fields.map(({ key, name: index, ...rest }) => (
{/* 左:变量选择器 */} ); }} {/* 右:输入控件,根据变量类型决定组件 */} { return prevValues.conditions?.[index]?.leftSelector !== currentValues.conditions?.[index]?.leftSelector; }} > {({ getFieldValue }) => { const leftSelector = getFieldValue(["conditions", index, "leftSelector"]); const valueType = getValueTypeBySelector(leftSelector); return ( {valueType === "string" ? ( ) : valueType === "number" ? ( ) : valueType === "boolean" ? ( ) : ( )} ); }}
))}
)}
); } ); const getInitialValues = (): ConditionFormValues => { return { conditions: [{}], logicalOperator: "and", }; }; export default BranchBlockNodeConfigExprInputBox; ================================================ FILE: ui/src/components/workflow/designer/forms/BranchBlockNodeConfigForm.tsx ================================================ import { useMemo, useRef } from "react"; import { getI18n, useTranslation } from "react-i18next"; import { type FlowNodeEntity } from "@flowgram.ai/fixed-layout-editor"; import { type AnchorProps, Form, type FormInstance } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { type Expr, type ExprComparisonOperator, type ExprLogicalOperator, ExprType, type ExprValueType, type WorkflowNodeConfigForBranchBlock, defaultNodeConfigForBranchBlock, } from "@/domain/workflow"; import { useAntdForm } from "@/hooks"; import { NodeFormContextProvider } from "./_context"; import BranchBlockNodeConfigExprInputBox, { type BranchBlockNodeConfigExprInputBoxInstance } from "./BranchBlockNodeConfigExprInputBox"; import { NodeType } from "../nodes/typings"; export interface BranchBlockNodeConfigFormProps { form: FormInstance; node: FlowNodeEntity; } const BranchBlockNodeConfigForm = ({ node, ...props }: BranchBlockNodeConfigFormProps) => { if (node.flowNodeType !== NodeType.BranchBlock) { console.warn(`[certimate] current workflow node type is not: ${NodeType.BranchBlock}`); } const { i18n, t } = useTranslation(); const initialValues = useMemo(() => { return node.form?.getValueIn("config") as WorkflowNodeConfigForBranchBlock | undefined; }, [node]); const formSchema = getSchema({ i18n }).superRefine(async (values, ctx) => { if (values.expression != null) { try { await exprInputBoxRef.current!.validate(); } catch { if (!ctx.issues.some((issue) => issue.path?.[0] === "expression")) { ctx.addIssue({ code: "custom", message: t("workflow_node.branch_block.form.expression.errmsg.invalid"), path: ["expression"], }); } } } }); const formRule = createSchemaFieldRule(formSchema); const { form: formInst, formProps } = useAntdForm>({ form: props.form, name: "workflowNodeBranchBlockConfigForm", initialValues: initialValues ?? getInitialValues(), }); const exprInputBoxRef = useRef(null); return (
); }; const getAnchorItems = ({ i18n = getI18n() }: { i18n?: ReturnType }): Required["items"] => { const { t } = i18n; return ["parameters"].map((key) => ({ key: key, title: t(`workflow_node.branch_block.form_anchor.${key}.tab`), href: "#" + key, })); }; const getInitialValues = (): Nullish>> => { return defaultNodeConfigForBranchBlock(); }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; const exprSchema: z.ZodType = z.lazy(() => z.discriminatedUnion("type", [ z.object({ type: z.literal(ExprType.Constant), value: z.string(), valueType: z.string(), }), z.object({ type: z.literal(ExprType.Variant), selector: z.object({ id: z.string(), name: z.string(), type: z.string(), }), }), z.object({ type: z.literal(ExprType.Comparison), operator: z.string(), left: exprSchema, right: exprSchema, }), z.object({ type: z.literal(ExprType.Logical), operator: z.string(), left: exprSchema, right: exprSchema, }), z.object({ type: z.literal(ExprType.Not), expr: exprSchema, }), ]) ); return z.object({ expression: z .any() .nullish() .refine((v) => v == null || exprSchema.safeParse(v).success, t("workflow_node.branch_block.form.expression.errmsg.invalid")), }); }; const _default = Object.assign(BranchBlockNodeConfigForm, { getAnchorItems, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/DelayNodeConfigDrawer.tsx ================================================ import { useTranslation } from "react-i18next"; import { type FlowNodeEntity } from "@flowgram.ai/fixed-layout-editor"; import { Form } from "antd"; import { NodeConfigDrawer } from "./_shared"; import DelayNodeConfigForm from "./DelayNodeConfigForm"; import { NodeType } from "../nodes/typings"; export interface DelayNodeConfigDrawerProps { afterClose?: () => void; loading?: boolean; node: FlowNodeEntity; open?: boolean; onOpenChange?: (open: boolean) => void; } const DelayNodeConfigDrawer = ({ node, ...props }: DelayNodeConfigDrawerProps) => { if (node.flowNodeType !== NodeType.Delay) { console.warn(`[certimate] current workflow node type is not: ${NodeType.Delay}`); } const { i18n } = useTranslation(); const [formInst] = Form.useForm(); return ( ); }; export default DelayNodeConfigDrawer; ================================================ FILE: ui/src/components/workflow/designer/forms/DelayNodeConfigForm.tsx ================================================ import { useMemo } from "react"; import { getI18n, useTranslation } from "react-i18next"; import { type FlowNodeEntity } from "@flowgram.ai/fixed-layout-editor"; import { type AnchorProps, Form, type FormInstance, InputNumber } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { type WorkflowNodeConfigForDelay, defaultNodeConfigForDelay } from "@/domain/workflow"; import { useAntdForm } from "@/hooks"; import { NodeFormContextProvider } from "./_context"; import { NodeType } from "../nodes/typings"; export interface DelayNodeConfigFormProps { form: FormInstance; node: FlowNodeEntity; } const DelayNodeConfigForm = ({ node, ...props }: DelayNodeConfigFormProps) => { if (node.flowNodeType !== NodeType.Delay) { console.warn(`[certimate] current workflow node type is not: ${NodeType.Delay}`); } const { i18n, t } = useTranslation(); const initialValues = useMemo(() => { return node.form?.getValueIn("config") as WorkflowNodeConfigForDelay | undefined; }, [node]); const formSchema = getSchema({ i18n }); const formRule = createSchemaFieldRule(formSchema); const { form: formInst, formProps } = useAntdForm>({ form: props.form, name: "workflowNodeDelayConfigForm", initialValues: initialValues ?? getInitialValues(), }); return (
); }; const getAnchorItems = ({ i18n = getI18n() }: { i18n?: ReturnType }): Required["items"] => { const { t } = i18n; return ["parameters"].map((key) => ({ key: key, title: t(`workflow_node.delay.form_anchor.${key}.tab`), href: "#" + key, })); }; const getInitialValues = (): Nullish>> => { return { ...(defaultNodeConfigForDelay() as Nullish>>), }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t: _ } = i18n; return z.object({ wait: z.coerce.number().int().positive(), }); }; const _default = Object.assign(DelayNodeConfigForm, { getAnchorItems, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/StartNodeConfigDrawer.tsx ================================================ import { useTranslation } from "react-i18next"; import { type FlowNodeEntity } from "@flowgram.ai/fixed-layout-editor"; import { Form } from "antd"; import { NodeConfigDrawer } from "./_shared"; import StartNodeConfigForm from "./StartNodeConfigForm"; import { NodeType } from "../nodes/typings"; export interface StartNodeConfigDrawerProps { afterClose?: () => void; loading?: boolean; node: FlowNodeEntity; open?: boolean; onOpenChange?: (open: boolean) => void; } const StartNodeConfigDrawer = ({ node, ...props }: StartNodeConfigDrawerProps) => { if (node.flowNodeType !== NodeType.Start) { console.warn(`[certimate] current workflow node type is not: ${NodeType.Start}`); } const { i18n } = useTranslation(); const [formInst] = Form.useForm(); return ( ); }; export default StartNodeConfigDrawer; ================================================ FILE: ui/src/components/workflow/designer/forms/StartNodeConfigForm.tsx ================================================ import { useEffect, useMemo, useState } from "react"; import { getI18n, useTranslation } from "react-i18next"; import { type FlowNodeEntity } from "@flowgram.ai/fixed-layout-editor"; import { IconDice6 } from "@tabler/icons-react"; import { type AnchorProps, Button, Form, type FormInstance, Input, Radio, Space } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import dayjs from "dayjs"; import { z } from "zod"; import Show from "@/components/Show"; import Tips from "@/components/Tips"; import { WORKFLOW_TRIGGERS, type WorkflowNodeConfigForStart, defaultNodeConfigForStart } from "@/domain/workflow"; import { useAntdForm } from "@/hooks"; import { getNextCronExecutions, validateCronExpression } from "@/utils/cron"; import { NodeFormContextProvider } from "./_context"; import { NodeType } from "../nodes/typings"; export interface StartNodeConfigFormProps { form: FormInstance; node: FlowNodeEntity; } const StartNodeConfigForm = ({ node, ...props }: StartNodeConfigFormProps) => { if (node.flowNodeType !== NodeType.Start) { console.warn(`[certimate] current workflow node type is not: ${NodeType.Start}`); } const { i18n, t } = useTranslation(); const initialValues = useMemo(() => { return node.form?.getValueIn("config") as WorkflowNodeConfigForStart | undefined; }, [node]); const formSchema = getSchema({ i18n }); const formRule = createSchemaFieldRule(formSchema); const { form: formInst, formProps } = useAntdForm>({ form: props.form, name: "workflowNodeStartConfigForm", initialValues: initialValues ?? getInitialValues(), }); const fieldTrigger = Form.useWatch("trigger", formInst); const fieldTriggerCron = Form.useWatch("triggerCron", formInst); const [fieldTriggerCronExpectedExecutions, setFieldTriggerCronExpectedExecutions] = useState([]); useEffect(() => { setFieldTriggerCronExpectedExecutions(getNextCronExecutions(fieldTriggerCron!, 5)); }, [fieldTriggerCron]); const handleTriggerChange = (value: string) => { if (value === WORKFLOW_TRIGGERS.SCHEDULED) { formInst.setFieldValue("triggerCron", initialValues?.triggerCron || "0 0 * * *"); } else { formInst.setFieldValue("triggerCron", void 0); } }; const handleRandomCronClick = () => { const m = Math.floor(Math.random() * 60); const h = Math.floor(Math.random() * 24); formInst.setFieldValue("triggerCron", `${m} ${h} * * *`); }; return (
handleTriggerChange(e.target.value)}> {t("workflow_node.start.form.trigger.option.manual.label")} {t("workflow_node.start.form.trigger.option.scheduled.label")} } />
); }; const getAnchorItems = ({ i18n = getI18n() }: { i18n?: ReturnType }): Required["items"] => { const { t } = i18n; return ["parameters"].map((key) => ({ key: key, title: t(`workflow_node.start.form_anchor.${key}.tab`), href: "#" + key, })); }; const getInitialValues = (): Nullish>> => { return { trigger: WORKFLOW_TRIGGERS.MANUAL, ...(defaultNodeConfigForStart() as Nullish>>), }; }; const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { const { t } = i18n; return z .object({ trigger: z.string().nonempty(t("workflow_node.start.form.trigger.placeholder")), triggerCron: z.string().nullish(), }) .superRefine((values, ctx) => { if (values.trigger === WORKFLOW_TRIGGERS.SCHEDULED) { if (!validateCronExpression(values.triggerCron!)) { ctx.addIssue({ code: "custom", message: t("workflow_node.start.form.trigger_cron.errmsg.invalid"), path: ["triggerCron"], }); } } }); }; const _default = Object.assign(StartNodeConfigForm, { getAnchorItems, getSchema, }); export default _default; ================================================ FILE: ui/src/components/workflow/designer/forms/_context.ts ================================================ import { createContext, useContext } from "react"; import { type FlowNodeEntity } from "@flowgram.ai/fixed-layout-editor"; // #region FormNestedFieldsContext export type FormNestedFieldsContextType = { parentNamePath: string; }; export const FormNestedFieldsContext = createContext({ parentNamePath: "", }); export const FormNestedFieldsContextProvider = FormNestedFieldsContext.Provider; export const useFormNestedFieldsContext = () => { const context = useContext(FormNestedFieldsContext); if (!context) { throw new Error("`FormNestedFieldsContext` must be used within a `FormNestedFieldsContextProvider`"); } return context; }; // #endregion // #region NodeFormContext export type NodeFormContextType = { node: FlowNodeEntity; }; export const NodeFormContext = createContext({} as NodeFormContextType); export const NodeFormContextProvider = NodeFormContext.Provider; export const useNodeFormContext = () => { const context = useContext(NodeFormContext); if (!context) { throw new Error("`NodeFormContext` must be used within a `NodeFormContextProvider`"); } return context; }; // #endregion ================================================ FILE: ui/src/components/workflow/designer/forms/_shared.tsx ================================================ import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { type FlowNodeEntity, useClientContext, useRefresh } from "@flowgram.ai/fixed-layout-editor"; import { IconChevronDown, IconEye, IconEyeOff, IconX } from "@tabler/icons-react"; import { useControllableValue } from "ahooks"; import { Anchor, type AnchorProps, App, Button, Drawer, Dropdown, Flex, type FormInstance, Space, Tooltip, Typography } from "antd"; import { isEqual } from "radash"; import Show from "@/components/Show"; import { unwrapErrMsg } from "@/utils/error"; import { type NodeRegistry } from "../nodes/typings"; export interface NodeConfigDrawerProps { children: React.ReactNode; afterClose?: () => void; anchor?: Pick, "items"> | false; footer?: boolean; form: FormInstance; loading?: boolean; node: FlowNodeEntity; open?: boolean; onOpenChange?: (open: boolean) => void; } export const NodeConfigDrawer = ({ children, afterClose, anchor, footer = true, form: formInst, loading, node, ...props }: NodeConfigDrawerProps) => { const { t } = useTranslation(); const ctx = useClientContext(); const { playground } = ctx; const refresh = useRefresh(); const { message, modal, notification } = App.useApp(); const [open, setOpen] = useControllableValue(props, { valuePropName: "open", defaultValuePropName: "defaultOpen", trigger: "onOpenChange", }); const containerRef = useRef(null); const [formPending, setFormPending] = useState(false); const submitForm = async () => { let formValues: Record; setFormPending(true); try { formValues = await formInst.validateFields(); } catch (err) { message.warning(t("common.errmsg.form_invalid")); setFormPending(false); throw err; } try { node.form!.setValueIn("config", formValues); node.form!.validate(); } catch (err) { notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); throw err; } finally { setFormPending(false); } }; const nodeRegistry = node?.getNodeRegistry(); const NodeIcon = nodeRegistry?.meta?.icon; const renderNodeIcon = () => NodeIcon == null ? null : (
); const [isNodeDisabled, setIsNodeDisabled] = useState(() => { if (node) { return node.form?.getValueIn("disabled"); } return false; }); useEffect(() => { const d1 = playground.config.onDataChange(() => refresh()); const d2 = node?.onDataChange(() => setIsNodeDisabled(node.form?.getValueIn("disabled"))); return () => { d1.dispose(); d2.dispose(); }; }); const handleOkClick = async () => { if (node == null) { setOpen(false); return; } await submitForm(); setOpen(false); }; const handleOkAndContinueClick = async () => { if (node == null) { setOpen(false); return; } await submitForm(); message.success(t("common.text.saved")); }; const handleCancelClick = () => { if (formPending) return; setOpen(false); }; const handleClose = () => { if (formPending) return; const picker = (obj: Record) => { return Object.entries(obj).reduce( (acc, [key, value]) => { const isEmpty = value == null || (typeof value === "string" && value === "") || (Array.isArray(value) && value.length === 0) || (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0); if (!isEmpty) { acc[key] = value; } return acc; }, {} as Record ); }; const oldValues = picker(node?.toJSON()?.data?.config ?? {}); const newValues = picker(formInst.getFieldsValue(true)); const changed = !isEqual(oldValues, {}) && !isEqual(oldValues, newValues); const { promise, resolve } = Promise.withResolvers(); if (changed) { modal.confirm({ title: t("common.text.operation_confirm"), content: t("workflow.detail.design.unsaved_changes.confirm"), onOk: () => resolve(void 0), }); } else { resolve(void 0); } promise.then(() => setOpen(false)); }; const handleDisableNodeClick = () => { node.form!.setValueIn("disabled", !isNodeDisabled); }; return ( !open && afterClose?.()} autoFocus closeIcon={false} destroyOnHidden footer={ footer ? ( handleTabChange(key)} />
columns={tableColumns} dataSource={tableData} loading={loading} locale={{ emptyText: loading ? ( ) : ( } extra={ loadError ? ( ) : ( ) } /> ), }} pagination={{ current: page, pageSize: pageSize, total: tableTotal, showSizeChanger: true, onChange: handlePaginationChange, onShowSizeChange: handlePaginationChange, }} rowClassName="cursor-pointer" rowKey={(record) => record.id} rowSelection={tableRowSelection} scroll={{ x: "max(100%, 960px)" }} onRow={(record) => ({ onClick: () => { handleRecordDetailClick(record); }, })} /> 0}>
); }; export default AccessList; ================================================ FILE: ui/src/pages/accesses/AccessNew.tsx ================================================ import { useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate, useSearchParams } from "react-router-dom"; import { useMount } from "ahooks"; import { App, Button, Flex, Form } from "antd"; import AccessForm, { type AccessFormUsages } from "@/components/access/AccessForm"; import AccessProviderPicker, { type AccessProviderPickerInstance } from "@/components/provider/AccessProviderPicker"; import Show from "@/components/Show"; import { type AccessModel } from "@/domain/access"; import { ACCESS_USAGES } from "@/domain/provider"; import { useZustandShallowSelector } from "@/hooks"; import { useAccessesStore } from "@/stores/access"; import { unwrapErrMsg } from "@/utils/error"; const AccessNew = () => { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const { t } = useTranslation(); const { notification } = App.useApp(); const { createAccess } = useAccessesStore(useZustandShallowSelector(["createAccess"])); const providerUsage = useMemo(() => searchParams.get("usage") as AccessFormUsages, [searchParams]); const providerFilter = AccessForm.useProviderFilterByUsage(providerUsage); const providerPickerRef = useRef(null); useMount(() => { setTimeout(() => { providerPickerRef.current?.inputRef?.focus(); }, 1); }); const [formInst] = Form.useForm(); const [formPending, setFormPending] = useState(false); const fieldProvider = Form.useWatch("provider", { form: formInst, preserve: true }); const handleProviderPick = (value: string) => { formInst.setFieldValue("provider", value); }; const handleSubmitClick = async () => { let formValues: AccessModel; setFormPending(true); try { formValues = await formInst.validateFields(); formValues.reserve = providerUsage === "ca" ? "ca" : providerUsage === "notification" ? "notif" : void 0; } catch (err) { setFormPending(false); throw err; } try { await createAccess(formValues); navigate(`/accesses?usage=${providerUsage}`, { replace: true }); } catch (err) { notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); throw err; } finally { setFormPending(false); } }; const handleCancelClick = () => { formInst.resetFields(); }; return (

{t("access.new.title")}

{t("access.new.subtitle")}

); }; export default AccessNew; ================================================ FILE: ui/src/pages/certificates/CertificateList.tsx ================================================ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate, useSearchParams } from "react-router-dom"; import { IconBrowserShare, IconCertificate, IconDots, IconExternalLink, IconReload, IconShieldCancel, IconTrash } from "@tabler/icons-react"; import { useMount, useRequest } from "ahooks"; import { App, Button, Dropdown, Input, Segmented, Skeleton, Table, type TableProps, Typography, theme } from "antd"; import dayjs from "dayjs"; import { ClientResponseError } from "pocketbase"; import { revoke as revokeCertificate } from "@/api/certificates"; import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer"; import Empty from "@/components/Empty"; import Show from "@/components/Show"; import { CERTIFICATE_SOURCES, type CertificateModel } from "@/domain/certificate"; import { useAppSettings, useZustandShallowSelector } from "@/hooks"; import { get as getCertificate, list as listCertificates, remove as removeCertificate } from "@/repository/certificate"; import { usePersistenceSettingsStore } from "@/stores/settings"; import { unwrapErrMsg } from "@/utils/error"; const CertificateList = () => { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const { t } = useTranslation(); const { token: themeToken } = theme.useToken(); const { message, modal, notification } = App.useApp(); const { appSettings: globalAppSettings } = useAppSettings(); const { settings: persistenceSettings, loadSettings: loadPersistenceSettings } = usePersistenceSettingsStore( useZustandShallowSelector(["settings", "loadSettings"]) ); useMount(() => loadPersistenceSettings(false)); const [expiryThreshold, setExpiryThreshold] = useState(() => persistenceSettings.certificatesWarningDaysBeforeExpire || 0); useEffect(() => { setExpiryThreshold(persistenceSettings.certificatesWarningDaysBeforeExpire || 0); }, [persistenceSettings.certificatesWarningDaysBeforeExpire]); const [filters, setFilters] = useState>(() => { return { keyword: searchParams.get("keyword"), state: searchParams.get("state"), }; }); const [sorter, setSorter] = useState["onChange"]>>[2]>>(() => { return {}; }); const [page, setPage] = useState(() => parseInt(+searchParams.get("page")! + "") || 1); const [pageSize, setPageSize] = useState(() => parseInt(+searchParams.get("perPage")! + "") || globalAppSettings.defaultPerPage!); const [tableData, setTableData] = useState([]); const [tableTotal, setTableTotal] = useState(0); const [tableSelectedRowKeys, setTableSelectedRowKeys] = useState([]); const tableColumns: TableProps["columns"] = [ { key: "name", title: t("certificate.props.subject_alt_names"), render: (_, record) => {record.subjectAltNames}, }, { key: "validity", title: t("certificate.props.validity"), sorter: true, sortOrder: sorter.columnKey === "validity" ? sorter.order : void 0, render: (_, record) => { const total = dayjs(record.validityNotAfter).diff(dayjs(record.validityNotBefore), "d") + 1; const isRevoked = record.isRevoked; const isExpired = dayjs().isAfter(dayjs(record.validityNotAfter)); const leftHours = dayjs(record.validityNotAfter).diff(dayjs(), "h"); const leftDays = Math.round(leftHours / 24); return (
{!isRevoked && !isExpired ? ( leftDays >= expiryThreshold ? (   {t("certificate.props.validity.left_days", { left: leftDays, total })} ) : (   {leftDays >= 1 ? t("certificate.props.validity.left_days", { left: leftDays, total }) : t("certificate.props.validity.less_than_a_day", { total })} ) ) : (   {isRevoked ? t("certificate.props.revoked") : t("certificate.props.validity.expired")} )} {t("certificate.props.validity.expiration", { date: dayjs(record.validityNotAfter).format("YYYY-MM-DD") })}
); }, }, { key: "brand", title: t("certificate.props.brand"), render: (_, record) => (
{record.issuerOrg || "\u00A0"} {record.keyAlgorithm || "\u00A0"}
), }, { key: "source", title: t("certificate.props.source"), render: (_, record) => { const workflowId = record.workflowRef; return (
{t(`certificate.props.source.${record.source}`)} { e.stopPropagation(); if (workflowId) { navigate(`/workflows/${workflowId}`); } }} > {record.expand?.workflowRef?.name ?? {`#${workflowId}`}}
); }, }, { key: "createdAt", title: t("certificate.props.created_at"), ellipsis: true, render: (_, record) => { return dayjs(record.created!).format("YYYY-MM-DD HH:mm:ss"); }, }, { key: "$action", align: "end", fixed: "right", width: 64, render: (_, record) => ( ), onClick: () => { handleRecordDetailClick(record); }, }, { key: "revoke", label: t("certificate.action.revoke.menu"), danger: true, disabled: record.source !== CERTIFICATE_SOURCES.REQUEST || record.isRevoked, icon: ( ), onClick: () => { handleRecordRevokeClick(record); }, }, { type: "divider", }, { key: "delete", label: t("certificate.action.delete.menu"), danger: true, icon: ( ), onClick: () => { handleRecordDeleteClick(record); }, }, ], }} trigger={["click"]} > ) : ( ) } /> ), }} pagination={{ current: page, pageSize: pageSize, total: tableTotal, showSizeChanger: true, onChange: handlePaginationChange, onShowSizeChange: handlePaginationChange, }} rowClassName="cursor-pointer" rowKey={(record) => record.id} rowSelection={tableRowSelection} scroll={{ x: "max(100%, 960px)" }} onChange={(_, __, sorter) => { setSorter(Array.isArray(sorter) ? sorter[0] : sorter); }} onRow={(record) => ({ onClick: () => { handleRecordDetailClick(record); }, })} /> 0}>
); }; export default CertificateList; ================================================ FILE: ui/src/pages/dashboard/Dashboard.tsx ================================================ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { IconActivity, IconAlertHexagon, IconBox, IconCertificate, IconCirclePlus, IconConfetti, IconExternalLink, IconHexagonLetterX, IconHistory, IconLock, IconPlugConnected, IconReload, IconRoute, IconShieldCheckered, } from "@tabler/icons-react"; import { useRequest } from "ahooks"; import { App, Button, Card, Col, Row, Skeleton, Table, type TableProps, Typography } from "antd"; import dayjs from "dayjs"; import { ClientResponseError } from "pocketbase"; import { get as getStatistics } from "@/api/statistics"; import Empty from "@/components/Empty"; import WorkflowRunDetailDrawer from "@/components/workflow/WorkflowRunDetailDrawer"; import WorkflowStatus from "@/components/workflow/WorkflowStatus"; import { APP_DOWNLOAD_URL } from "@/domain/app"; import { type Statistics } from "@/domain/statistics"; import { type WorkflowRunModel } from "@/domain/workflowRun"; import { useBrowserTheme, useVersionChecker } from "@/hooks"; import { get as getWorkflowRun, list as listWorkflowRuns } from "@/repository/workflowRun"; import { mergeCls } from "@/utils/css"; import { unwrapErrMsg } from "@/utils/error"; const Dashboard = () => { const { t } = useTranslation(); return (

{t("dashboard.page.title")}

{t("dashboard.shortcut")}

{t("dashboard.recent_workflow_runs")}

); }; const StatisticCard = ({ className, style, label, loading, icon, value, onClick, }: { className?: string; style?: React.CSSProperties; label: React.ReactNode; loading?: boolean; icon: React.ReactNode; value?: string | number | React.ReactNode; onClick?: () => void; }) => { return (
{label}
{value}
{icon}
); }; const StatisticCards = ({ className, style }: { className?: string; style?: React.CSSProperties }) => { const navigate = useNavigate(); const { t } = useTranslation(); const { theme: browserTheme } = useBrowserTheme(); const { notification } = App.useApp(); const cardGridSpans = { xs: { flex: "50%" }, md: { flex: "50%" }, lg: { flex: "33.3333%" }, xl: { flex: "33.3333%" }, xxl: { flex: "20%" }, }; const cardStylesFn = (color: string) => ({ background: browserTheme === "dark" ? `linear-gradient(135deg, color-mix(in srgb, ${color} 50%, black 20%) 0%, color-mix(in srgb, ${color} 50%, white 20%) 100%)` : `linear-gradient(135deg, color-mix(in srgb, ${color} 80%, black 30%) 0%, color-mix(in srgb, ${color} 80%, white 30%) 100%)`, }); const [statistics, setStatistics] = useState(); const { loading } = useRequest( () => { return getStatistics(); }, { onSuccess: (res) => { setStatistics(res.data); }, onError: (err) => { if (err instanceof ClientResponseError && err.isAbort) { return; } console.error(err); notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); throw err; }, } ); return (
} label={t("dashboard.statistics.all_certificates")} loading={loading} value={statistics?.certificateTotal ?? "-"} onClick={() => navigate("/certificates")} /> } label={t("dashboard.statistics.expiring_soon_certificates")} loading={loading} value={statistics?.certificateExpiringSoon ?? "-"} onClick={() => navigate("/certificates?state=expiringSoon")} /> } label={t("dashboard.statistics.expired_certificates")} loading={loading} value={statistics?.certificateExpired ?? "-"} onClick={() => navigate("/certificates?state=expired")} /> } label={t("dashboard.statistics.all_workflows")} loading={loading} value={statistics?.workflowTotal ?? "-"} onClick={() => navigate("/workflows")} /> } label={t("dashboard.statistics.enabled_workflows")} loading={loading} value={statistics?.workflowEnabled ?? "-"} onClick={() => navigate("/workflows?state=enabled")} />
); }; const Shortcuts = ({ className, style }: { className?: string; style?: React.CSSProperties }) => { const navigate = useNavigate(); const { t } = useTranslation(); const { hasUpdate } = useVersionChecker(); return (
{hasUpdate && ( )}
); }; const WorkflowRunHistoryTable = ({ className, style }: { className?: string; style?: React.CSSProperties }) => { const navigate = useNavigate(); const { t } = useTranslation(); const { notification } = App.useApp(); const [tableData, setTableData] = useState([]); const tableColumns: TableProps["columns"] = [ { key: "$index", align: "center", fixed: "left", width: 48, render: (_, __, index) => index + 1, }, { key: "id", title: "ID", width: 160, render: (_, record) => {record.id}, }, { key: "workflow", title: t("workflow_run.props.workflow"), render: (_, record) => { const workflow = record.expand?.workflowRef; return (
{ if (workflow) { navigate(`/workflows/${workflow.id}`); } }} > {workflow?.name ?? {`#${record.workflowRef}`}}
); }, }, { key: "status", title: t("workflow_run.props.status"), render: (_, record) => { return ; }, }, { key: "startedAt", title: t("workflow_run.props.started_at"), ellipsis: true, render: (_, record) => { if (record.startedAt) { return dayjs(record.startedAt).format("YYYY-MM-DD HH:mm:ss"); } return <>; }, }, { key: "endedAt", title: t("workflow_run.props.ended_at"), ellipsis: true, render: (_, record) => { if (record.endedAt) { return dayjs(record.endedAt).format("YYYY-MM-DD HH:mm:ss"); } return <>; }, }, { key: "artifacts", title: t("workflow_run.props.artifacts"), width: 160, render: (_, record) => { if (record.outputs && record.outputs.length > 0) { const keys = new Set(); const icons: React.ReactNode[] = []; for (const output of record.outputs) { if (output.type === "ref" && output.value?.split("#")?.at(0) === "certificate") { const KEY = "certificate"; if (keys.has(KEY)) continue; keys.add(KEY); icons.push(); } else { const KEY = "other"; if (keys.has(KEY)) continue; keys.add(KEY); icons.push(); } } return
{icons}
; } return <>; }, }, ]; const { loading, error: loadError, run: refreshData, } = useRequest( () => { return listWorkflowRuns({ page: 1, perPage: 15, expand: true, }); }, { onSuccess: (res) => { setTableData(res.items); }, onError: (err) => { if (err instanceof ClientResponseError && err.isAbort) { return; } console.error(err); notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); throw err; }, } ); const handleReloadClick = () => { if (loading) return; refreshData(); }; const { drawerProps: detailDrawerProps, ...detailDrawer } = WorkflowRunDetailDrawer.useDrawer(); const handleRecordDetailClick = (workflowRun: WorkflowRunModel) => { const drawer = detailDrawer.open({ data: workflowRun, loading: true }); getWorkflowRun(workflowRun.id).then((data) => { drawer.safeUpdate({ data, loading: false }); }); }; return (
columns={tableColumns} dataSource={tableData} loading={loading} locale={{ emptyText: loading ? ( ) : ( } extra={ loadError ? ( ) : ( ) } /> ), }} pagination={false} rowClassName="cursor-pointer" rowKey={(record) => record.id} scroll={{ x: "max(100%, 720px)" }} onRow={(record) => ({ onClick: () => { handleRecordDetailClick(record); }, })} />
); }; export default Dashboard; ================================================ FILE: ui/src/pages/login/Login.tsx ================================================ import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { IconArrowRight, IconLock, IconMail } from "@tabler/icons-react"; import { App, Button, Card, Divider, Form, Input, Space } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import AppDocument from "@/components/AppDocument"; import AppLocale from "@/components/AppLocale"; import AppTheme from "@/components/AppTheme"; import AppVersion from "@/components/AppVersion"; import { useAntdForm, useBrowserTheme } from "@/hooks"; import { authWithPassword } from "@/repository/admin"; import { unwrapErrMsg } from "@/utils/error"; const Login = () => { const navigage = useNavigate(); const { t } = useTranslation(); const { notification } = App.useApp(); const { theme: browserTheme } = useBrowserTheme(); const bgStyle = useMemo(() => { let svg = ""; let mask = ""; if (browserTheme === "dark") { svg = ``; mask = "white"; } else { svg = ``; mask = "black"; } return { backgroundImage: `url('data:image/svg+xml;base64,${btoa(svg)}')`, maskImage: `linear-gradient(to bottom right, transparent, ${mask}, transparent)`, }; }, [browserTheme]); const formSchema = z.object({ username: z.email(t("login.username.errmsg.invalid")), password: z.string().min(10, t("login.password.errmsg.invalid")), }); const formRule = createSchemaFieldRule(formSchema); const { form: formInst, formPending, formProps, } = useAntdForm>({ initialValues: { username: "", password: "", }, onSubmit: async (values) => { try { await authWithPassword(values.username, values.password); await navigage("/"); } catch (err) { notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); throw err; } }, }); return ( <>
} size={4}>
} size={4}>
} size={4}>
); }; export default Login; ================================================ FILE: ui/src/pages/presets/PresetList.tsx ================================================ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useSearchParams } from "react-router-dom"; import { Tabs } from "antd"; import Show from "@/components/Show"; import PresetListNotifyTemplates from "./PresetListNotifyTemplates"; import PresetListScriptTemplates from "./PresetListScriptTemplates"; type PresetUsages = "notification" | "script"; const PresetList = () => { const [searchParams, setSearchParams] = useSearchParams(); const { t } = useTranslation(); const [tabKey, setTabKey] = useState(() => { return (searchParams.get("usage") || "notification") as PresetUsages; }); const handleTabChange = (key: string) => { setTabKey(key as PresetUsages); setSearchParams((prev) => { prev.set("usage", key); return prev; }); }; return (

{t("preset.page.title")}

{t("preset.page.subtitle")}

handleTabChange(key)} />
); }; export default PresetList; ================================================ FILE: ui/src/pages/presets/PresetListNotifyTemplates.tsx ================================================ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { IconDots, IconEdit, IconPlus, IconTrash } from "@tabler/icons-react"; import { useControllableValue, useMount } from "ahooks"; import { App, Button, Card, Divider, Dropdown, Form, Input, Typography } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { nanoid } from "nanoid/non-secure"; import { ClientResponseError } from "pocketbase"; import { z } from "zod"; import DrawerForm from "@/components/DrawerForm"; import Tips from "@/components/Tips"; import { useAntdForm, useZustandShallowSelector } from "@/hooks"; import { useNotifyTemplatesStore } from "@/stores/settings"; import { unwrapErrMsg } from "@/utils/error"; const MAX_TEMPLATE_COUNT = 99; type PresetTemplate = { name: string; subject: string; message: string; }; const PresetListNotifyTemplates = () => { const { t } = useTranslation(); const { message, modal, notification } = App.useApp(); const { templates, loading, loadedAtOnce, fetchTemplates, setTemplates, addTemplate, removeTemplateByIndex } = useNotifyTemplatesStore(); useMount(() => { fetchTemplates().catch((err) => { if (err instanceof ClientResponseError && err.isAbort) { return; } console.error(err); notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); }); }); const [createDrawerOpen, setCreateDrawerOpen] = useState(false); const [detailDrawerOpen, setDetailDrawerOpen] = useState(false); const [detailDrawerRecord, setDetailDrawerRecord] = useState(); const [detailDrawerIndex, setDetailDrawerIndex] = useState(); const handleCreateClick = () => { if (!loadedAtOnce) return; if (templates.length >= MAX_TEMPLATE_COUNT) { message.warning(t("preset.warning.excceeded")); return; } setCreateDrawerOpen(true); }; const handleRecordDetailClick = (template: PresetTemplate, index: number) => { setDetailDrawerIndex(index); setDetailDrawerRecord({ ...template }); setDetailDrawerOpen(true); }; const handleRecordDeleteClick = (template: PresetTemplate, index: number) => { modal.confirm({ title: {t("preset.action.delete.modal.title", { name: template.name })}, content: , icon: ( ), okText: t("common.button.confirm"), okButtonProps: { danger: true }, onOk: async () => { try { await removeTemplateByIndex(index); } catch (err) { console.error(err); notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); } }, }); }; const handleCreateDrawerSubmit = async (values: PresetTemplate) => { try { await addTemplate(values); setCreateDrawerOpen(false); } catch (err) { console.error(err); notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); } }; const handleModifyDrawerSubmit = async (values: PresetTemplate) => { try { const newTemplates = [...templates]; newTemplates[detailDrawerIndex!] = values; await setTemplates(newTemplates); setDetailDrawerIndex(void 0); setDetailDrawerRecord(void 0); setDetailDrawerOpen(false); } catch (err) { console.error(err); notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); } }; return ( <> } />
{t("preset.action.create.button")}
{templates.map((template, index) => (
), onClick: (e) => { e.domEvent.stopPropagation(); handleRecordDetailClick(template, index); }, }, { type: "divider", }, { key: "delete", label: t("preset.action.delete.menu"), danger: true, icon: ( ), onClick: (e) => { e.domEvent.stopPropagation(); handleRecordDeleteClick(template, index); }, }, ], }} trigger={["click"]} >
))} {loading && !loadedAtOnce && (
)}
setCreateDrawerOpen(false)} onOpenChange={(open) => setCreateDrawerOpen(open)} onSubmit={handleCreateDrawerSubmit} /> setDetailDrawerOpen(false)} onOpenChange={(open) => setDetailDrawerOpen(open)} onSubmit={handleModifyDrawerSubmit} /> ); }; const InternalEditDrawer = ({ mode, data, onSubmit, ...props }: { afterClose?: () => void; mode: "create" | "modify"; data?: Nullish; open: boolean; onOpenChange?: (open: boolean) => void; onSubmit?: (record: PresetTemplate) => void; }) => { const { t } = useTranslation(); const { templates } = useNotifyTemplatesStore(useZustandShallowSelector(["templates"])); const [open, setOpen] = useControllableValue(props, { valuePropName: "open", defaultValuePropName: "defaultOpen", trigger: "onOpenChange", }); const afterClose = () => { formInst.resetFields(); props.afterClose?.(); }; const formSchema = z .object({ name: z.string().nonempty(t("preset.form.name.placeholder")), subject: z.string().nonempty(t("preset.form.notification_subject.placeholder")), message: z.string().nonempty(t("preset.form.notification_message.placeholder")), }) .superRefine((values, ctx) => { if (values.name) { const name = values.name.trim(); const duplicatedCount = templates.filter((t) => t.name.trim() === name).length; if (duplicatedCount > (mode === "create" ? 0 : 1)) { ctx.addIssue({ code: "custom", message: t("preset.form.name.errmsg.duplicated"), path: ["name"], }); } } }); const formRule = createSchemaFieldRule(formSchema); const { form: formInst, formProps } = useAntdForm>({ name: "viewPresetListNotifyTemplates_InternalDrawerForm_" + nanoid(), initialValues: data, }); const handleFormFinish = async (values: z.infer) => { switch (mode) { case "create": case "modify": { await onSubmit?.(values); } break; default: throw "Invalid props: `mode`"; } setOpen(false); }; return ( !open && afterClose?.() }} form={formInst} layout="vertical" okText={mode === "create" ? t("common.button.create") : mode === "modify" ? t("common.button.save") : void 0} open={open} preserve={false} title={mode === "create" ? t("preset.action.create.modal.title") : mode === "modify" ? t("preset.action.modify.modal.title") : void 0} validateTrigger="onSubmit" onFinish={handleFormFinish} onOpenChange={props.onOpenChange} > } /> ); }; export default PresetListNotifyTemplates; ================================================ FILE: ui/src/pages/presets/PresetListScriptTemplates.tsx ================================================ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { IconDots, IconEdit, IconPlus, IconTrash } from "@tabler/icons-react"; import { useControllableValue, useMount } from "ahooks"; import { App, Button, Card, Dropdown, Form, Input, Typography } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { nanoid } from "nanoid/non-secure"; import { ClientResponseError } from "pocketbase"; import { z } from "zod"; import CodeTextInput from "@/components/CodeTextInput"; import DrawerForm from "@/components/DrawerForm"; import Tips from "@/components/Tips"; import { useAntdForm, useZustandShallowSelector } from "@/hooks"; import { useScriptTemplatesStore } from "@/stores/settings"; import { unwrapErrMsg } from "@/utils/error"; const MAX_TEMPLATE_COUNT = 99; type PresetTemplate = { name: string; command: string; }; const PresetListScriptTemplates = () => { const { t } = useTranslation(); const { message, modal, notification } = App.useApp(); const { templates, loading, loadedAtOnce, fetchTemplates, setTemplates, addTemplate, removeTemplateByIndex } = useScriptTemplatesStore(); useMount(() => { fetchTemplates().catch((err) => { if (err instanceof ClientResponseError && err.isAbort) { return; } console.error(err); notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); }); }); const [createDrawerOpen, setCreateDrawerOpen] = useState(false); const [detailDrawerOpen, setDetailDrawerOpen] = useState(false); const [detailDrawerRecord, setDetailDrawerRecord] = useState(); const [detailDrawerIndex, setDetailDrawerIndex] = useState(); const handleCreateClick = () => { if (!loadedAtOnce) return; if (templates.length >= MAX_TEMPLATE_COUNT) { message.warning(t("preset.warning.excceeded")); return; } setCreateDrawerOpen(true); }; const handleRecordDetailClick = (template: PresetTemplate, index: number) => { setDetailDrawerIndex(index); setDetailDrawerRecord({ ...template }); setDetailDrawerOpen(true); }; const handleRecordDeleteClick = (template: PresetTemplate, index: number) => { modal.confirm({ title: {t("preset.action.delete.modal.title", { name: template.name })}, content: , icon: ( ), okText: t("common.button.confirm"), okButtonProps: { danger: true }, onOk: async () => { try { await removeTemplateByIndex(index); } catch (err) { console.error(err); notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); } }, }); }; const handleCreateDrawerSubmit = async (values: PresetTemplate) => { try { await addTemplate(values); setCreateDrawerOpen(false); } catch (err) { console.error(err); notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); } }; const handleModifyDrawerSubmit = async (values: PresetTemplate) => { try { const newTemplates = [...templates]; newTemplates[detailDrawerIndex!] = values; await setTemplates(newTemplates); setDetailDrawerIndex(void 0); setDetailDrawerRecord(void 0); setDetailDrawerOpen(false); } catch (err) { console.error(err); notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); } }; return ( <> } />
{t("preset.action.create.button")}
{templates.map((template, index) => (
), onClick: (e) => { e.domEvent.stopPropagation(); handleRecordDetailClick(template, index); }, }, { type: "divider", }, { key: "delete", label: t("preset.action.delete.menu"), danger: true, icon: ( ), onClick: (e) => { e.domEvent.stopPropagation(); handleRecordDeleteClick(template, index); }, }, ], }} trigger={["click"]} >
))} {loading && !loadedAtOnce && (
)}
setCreateDrawerOpen(false)} onOpenChange={(open) => setCreateDrawerOpen(open)} onSubmit={handleCreateDrawerSubmit} /> setDetailDrawerOpen(false)} onOpenChange={(open) => setDetailDrawerOpen(open)} onSubmit={handleModifyDrawerSubmit} /> ); }; const InternalEditDrawer = ({ mode, data, onSubmit, ...props }: { afterClose?: () => void; mode: "create" | "modify"; data?: Nullish; open: boolean; onOpenChange?: (open: boolean) => void; onSubmit?: (record: PresetTemplate) => void; }) => { const { t } = useTranslation(); const { templates } = useScriptTemplatesStore(useZustandShallowSelector(["templates"])); const [open, setOpen] = useControllableValue(props, { valuePropName: "open", defaultValuePropName: "defaultOpen", trigger: "onOpenChange", }); const afterClose = () => { formInst.resetFields(); props.afterClose?.(); }; const formSchema = z .object({ name: z.string().nonempty(t("preset.form.name.placeholder")), command: z.string().nonempty(t("preset.form.script_command.placeholder")), }) .superRefine((values, ctx) => { if (values.name) { const name = values.name.trim(); const duplicatedCount = templates.filter((t) => t.name.trim() === name).length; if (duplicatedCount > (mode === "create" ? 0 : 1)) { ctx.addIssue({ code: "custom", message: t("preset.form.name.errmsg.duplicated"), path: ["name"], }); } } }); const formRule = createSchemaFieldRule(formSchema); const { form: formInst, formProps } = useAntdForm>({ name: "viewPresetListScriptTemplates_InternalDrawerForm_" + nanoid(), initialValues: data, }); const handleFormFinish = async (values: z.infer) => { switch (mode) { case "create": case "modify": { await onSubmit?.(values); } break; default: throw "Invalid props: `mode`"; } setOpen(false); }; return ( !open && afterClose?.() }} form={formInst} layout="vertical" okText={mode === "create" ? t("common.button.create") : mode === "modify" ? t("common.button.save") : void 0} open={open} preserve={false} title={mode === "create" ? t("preset.action.create.modal.title") : mode === "modify" ? t("preset.action.modify.modal.title") : void 0} validateTrigger="onSubmit" onFinish={handleFormFinish} onOpenChange={props.onOpenChange} > ); }; export default PresetListScriptTemplates; ================================================ FILE: ui/src/pages/settings/Settings.tsx ================================================ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Outlet, useLocation, useNavigate } from "react-router-dom"; import { IconDatabaseCog, IconHeartRateMonitor, IconInfoCircle, IconPalette, IconPlugConnected, IconUserShield } from "@tabler/icons-react"; import { Menu } from "antd"; const Settings = () => { const location = useLocation(); const navigate = useNavigate(); const { t } = useTranslation(); const menus = [ ["account", "settings.account.tab", ], ["appearance", "settings.appearance.tab", ], ["ssl-provider", "settings.sslprovider.tab", ], ["persistence", "settings.persistence.tab", ], ["diagnostics", "settings.diagnostics.tab", ], ["about", "settings.about.tab", ], ] satisfies [string, string, React.ReactElement][]; const [menuKey, setMenuKey] = useState(() => location.pathname.split("/")[2]); useEffect(() => { const subpath = location.pathname.split("/")[2]; if (!subpath) { navigate("/settings/account"); return; } setMenuKey(subpath); }, [location.pathname]); const handleMenuClick = ({ key }: { key: string }) => { setMenuKey(key); navigate(`/settings/${key}`); }; return (

{t("settings.page.title")}

({ key, label: t(label), icon: ( {icon} ), }))} onClick={handleMenuClick} />
({ key, label: t(label), icon: ( {icon} ), }))} onClick={handleMenuClick} />
); }; export default Settings; ================================================ FILE: ui/src/pages/settings/SettingsAbout.tsx ================================================ import { useTranslation } from "react-i18next"; import { IconBook, IconBrandGithub, IconBrandTelegram, IconCoin, IconMessageChatbot } from "@tabler/icons-react"; import { Badge, Button, Divider, List, Tooltip, Typography } from "antd"; import { APP_DOCUMENT_URL, APP_DOWNLOAD_URL, APP_REPO_URL, APP_VERSION } from "@/domain/app"; import { useVersionChecker } from "@/hooks"; const SettingsAbout = () => { const { t } = useTranslation(); const { hasUpdate } = useVersionChecker(); const handleDownloadClick = () => { window.open(APP_DOWNLOAD_URL, "_blank"); }; const handleDocumentClick = () => { window.open(APP_DOCUMENT_URL, "_blank"); }; const handleGithubClick = () => { window.open(APP_REPO_URL, "_blank"); }; const handleTelegramClick = () => { window.open("https://t.me/+ZXphsppxUg41YmVl", "_blank"); }; const handleDonateClick = () => { window.open("https://profile.ikit.fun/sponsors/", "_blank"); }; const handleFeedbackClick = () => { window.open(APP_REPO_URL + "/issues", "_blank"); }; return ( <>

Certimate

Version: {APP_VERSION}

{t("settings.about.contributors.title")}

{t("settings.about.contributors.tips")}
Contributors
{t("settings.about.feedback.button")}}> } title={t("settings.about.feedback.title")} description={t("settings.about.feedback.subtitle")} />
); }; export default SettingsAbout; ================================================ FILE: ui/src/pages/settings/SettingsAccount.tsx ================================================ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { App, Button, Divider, Flex, Form, Input, Typography } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { useAntdForm } from "@/hooks"; import { authWithPassword, getAuthStore, save as saveAdmin } from "@/repository/admin"; import { unwrapErrMsg } from "@/utils/error"; const SettingsAccount = () => { const { t } = useTranslation(); return ( <>

{t("settings.account.username.title")}

{t("settings.account.password.title")}

{/*

{t("settings.account.2fa.title")}

TODO ...
*/} ); }; const SettingsAccountUsername = ({ className, style }: { className?: string; style?: React.CSSProperties }) => { const navigate = useNavigate(); const { t } = useTranslation(); const { message, notification } = App.useApp(); const formSchema = z.object({ username: z.email(t("common.errmsg.email_invalid")).max(256, t("common.errmsg.string_max", { max: 256 })), }); const formRule = createSchemaFieldRule(formSchema); const { form: formInst, formPending, formProps, } = useAntdForm>({ initialValues: { username: getAuthStore().record?.email, }, onSubmit: async (values) => { try { await saveAdmin({ email: values.username }); message.success(t("common.text.operation_succeeded")); setTimeout(() => { getAuthStore().clear(); navigate("/login"); }, 500); } catch (err) { notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); throw err; } }, }); const [formVisible, setFormVisible] = useState(false); const [formChanged, setFormChanged] = useState(false); const handleInputChange = () => { setFormChanged(formInst.getFieldValue("username") !== formProps.initialValues?.username); }; const handleEditClick = () => { setFormVisible(true); formInst.resetFields(); }; const handleCancelClick = () => { setFormVisible(false); setFormChanged(false); }; return (
{formVisible ? ( <> ) : ( <>
{t("settings.account.username.tips")}
{getAuthStore().record?.email}
)}
); }; const SettingsAccountPassword = ({ className, style }: { className?: string; style?: React.CSSProperties }) => { const navigate = useNavigate(); const { t } = useTranslation(); const { message, notification } = App.useApp(); const formSchema = z.object({ oldPassword: z .string() .min(10, t("settings.account.password.form.email.password.errmsg.invalid")) .max(256, t("common.errmsg.string_max", { max: 256 })), newPassword: z .string() .min(10, t("settings.account.password.form.email.password.errmsg.invalid")) .max(256, t("common.errmsg.string_max", { max: 256 })), confirmPassword: z.string().refine((v) => v === formInst.getFieldValue("newPassword"), { error: t("settings.account.password.form.email.password.errmsg.not_matched"), }), }); const formRule = createSchemaFieldRule(formSchema); const { form: formInst, formPending, formProps, } = useAntdForm({ initialValues: { oldPassword: "", newPassword: "", confirmPassword: "", }, onSubmit: async (values) => { try { await authWithPassword(getAuthStore().record!.email, values.oldPassword); await saveAdmin({ password: values.newPassword, passwordConfirm: values.confirmPassword }); message.success(t("common.text.operation_succeeded")); setTimeout(() => { getAuthStore().clear(); navigate("/login"); }, 500); } catch (err) { notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); throw err; } }, }); const [formVisible, setFormVisible] = useState(false); const [formChanged, setFormChanged] = useState(false); const handleInputChange = () => { const values = formInst.getFieldsValue(); setFormChanged(!!values.oldPassword && !!values.newPassword && !!values.confirmPassword); }; const handleEditClick = () => { setFormVisible(true); formInst.resetFields(); }; const handleCancelClick = () => { setFormVisible(false); setFormChanged(false); }; return (
{formVisible ? ( <> ) : ( <>
{t("settings.account.password.tips")}
)}
); }; export default SettingsAccount; ================================================ FILE: ui/src/pages/settings/SettingsAppearance.tsx ================================================ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { Divider, Form, Radio, type RadioChangeEvent, Select, theme } from "antd"; import { produce } from "immer"; import { useAppLocaleMenuItems } from "@/components/AppLocale"; import { useAppThemeMenuItems } from "@/components/AppTheme"; import { useAppSettings, useBrowserTheme } from "@/hooks"; const SettingsAppearance = () => { const { t } = useTranslation(); return ( <>

{t("settings.appearance.theme.title")}

{t("settings.appearance.language.title")}

{t("settings.appearance.pagination.title")}

{t("settings.appearance.workflow.title")}

); }; const SettingsAppearanceTheme = ({ className, style }: { className?: string; style?: React.CSSProperties }) => { const { t } = useTranslation(); const { token: themeToken } = theme.useToken(); const { themeMode, setThemeMode } = useBrowserTheme(); const themeItems = useAppThemeMenuItems(); const [themeChanged, setThemeChanged] = useState(false); const handleChange = (e: RadioChangeEvent) => { if (e.target.value !== themeMode) { setThemeChanged(true); setThemeMode(e.target.value); } }; return (
{themeItems.map((item) => (
{item.label}
))}
); }; const SettingsAppearanceLanguage = ({ className, style }: { className?: string; style?: React.CSSProperties }) => { const { i18n, t } = useTranslation(); const localeItems = useAppLocaleMenuItems(); const [localeChanged, setLocaleChanged] = useState(false); const handleChange = (value: string) => { if (value !== (i18n.resolvedLanguage ?? i18n.language)) { setLocaleChanged(true); i18n.changeLanguage(value); } }; return (
({ key: value, value: value, label: `${value} ${t("settings.appearance.pagination.form.default_per_page.unit")}`, }))} placeholder={t("settings.appearance.pagination.form.default_per_page.placeholder")} defaultValue={globalAppSettings.defaultPerPage} onChange={handleChange} />
); }; const SettingsAppearanceWorkflow = ({ className, style }: { className?: string; style?: React.CSSProperties }) => { const { t } = useTranslation(); const { appSettings: globalAppSettings, setAppSettings: setGlobalAppSettings } = useAppSettings(); const handleChange = (value: (typeof globalAppSettings)["defaultWorkflowLayout"]) => { setGlobalAppSettings( produce(globalAppSettings, (draft) => { draft.defaultWorkflowLayout = value; }) ); }; return (
); }; const InternalSettingsContext = createContext( {} as { loading: boolean; settings: SSLProviderSettingsContent; updateSettings: (settings: SSLProviderSettingsContent) => Promise; } ); const InternalCASharedForm = ({ children, provider }: { children?: React.ReactNode; provider: CAProviderType }) => { const { t } = useTranslation(); const { settings, updateSettings } = useContext(InternalSettingsContext); const { form: formInst, formProps } = useAntdForm>({ initialValues: settings?.configs?.[provider], onSubmit: async (values) => { setFormPending(true); try { const newSettings = produce(settings, (draft) => { draft.provider = provider; draft.configs ??= {} as SSLProviderSettingsContent["configs"]; draft.configs[provider] = values; }); await updateSettings(newSettings); } finally { setFormPending(false); } setFormChanged(false); }, }); const [formPending, setFormPending] = useState(false); const [formChanged, setFormChanged] = useState(false); useEffect(() => { setFormChanged(provider !== settings?.provider); }, [provider, settings?.provider]); const handleFormChange = () => { setFormChanged(true); }; return (
{children}
); }; const InternalCASharedFormEabFields = ({ i18nKey }: { i18nKey: string }) => { const { t, i18n } = useTranslation(); const hasGuide = i18n.exists(`access.form.${i18nKey}_eab.guide`); const formSchema = z.object({ endpoint: z.url(t("common.errmsg.url_invalid")), eabKid: z.string(t("access.form.shared_acme_eab_kid.label")).nonempty(t("access.form.shared_acme_eab_kid.placeholder")), eabHmacKey: z.string(t("access.form.shared_acme_eab_hmac_key.label")).nonempty(t("access.form.shared_acme_eab_hmac_key.placeholder")), }); const formRule = createSchemaFieldRule(formSchema); return ( <> ); }; const InternalCASettingsFormProviderLetsEncrypt = () => { return ; }; const InternalCASettingsFormProviderLetsEncryptStaging = () => { const { t } = useTranslation(); return ( } /> ); }; const InternalCASettingsFormProviderActalisSSL = () => { return ( ); }; const InternalCASettingsFormProviderGlobalSignAtlas = () => { return ( ); }; const InternalCASettingsFormProviderGoogleTrustServices = () => { return ( ); }; const InternalCASettingsFormProviderLiteSSL = () => { return ( ); }; const InternalCASettingsFormProviderSectigo = () => { const { t } = useTranslation(); const formSchema = z.object({ validationType: z.string().nonempty(t("access.form.sectigo_validation_type.placeholder")), }); const formRule = createSchemaFieldRule(formSchema); return ( } > } > ); }; export default SettingsSSLProvider; ================================================ FILE: ui/src/pages/workflows/WorkflowDetail.tsx ================================================ import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Outlet, useLocation, useNavigate, useParams } from "react-router-dom"; import { IconEdit, IconHistory, IconPlayerPlay, IconRobot } from "@tabler/icons-react"; import { useSize } from "ahooks"; import { App, Button, Input, type InputRef, Segmented, Skeleton, Spin } from "antd"; import { startRun as startWorkflowRun } from "@/api/workflows"; import Show from "@/components/Show"; import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun"; import { useZustandShallowSelector } from "@/hooks"; import { useWorkflowStore } from "@/stores/workflow"; import { mergeCls } from "@/utils/css"; import { unwrapErrMsg } from "@/utils/error"; const WorkflowDetail = () => { const location = useLocation(); const navigate = useNavigate(); const { t } = useTranslation(); const { message, modal, notification } = App.useApp(); const { id: workflowId } = useParams(); const { workflow, initialized, ...workflowState } = useWorkflowStore(useZustandShallowSelector(["workflow", "initialized", "init", "destroy", "setEnabled"])); useEffect(() => { Promise.try(() => workflowState.init(workflowId!)).catch((err) => { console.error(err); notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); }); return () => { workflowState.destroy(); }; }, [workflowId]); const divHeaderRef = useRef(null); const divHeaderSize = useSize(divHeaderRef); const tabs = [ ["design", "workflow.detail.design.tab", ], ["runs", "workflow.detail.runs.tab", ], ] satisfies [string, string, React.ReactElement][]; const [tabValue, setTabValue] = useState(() => location.pathname.split("/")[3]); useEffect(() => { const subpath = location.pathname.split("/")[3]; if (!subpath) { navigate(`/workflows/${workflowId}/${tabs[0][0]}`, { replace: true }); return; } setTabValue(subpath); }, [location.pathname, workflowId]); const handleTabChange = (value: string) => { setTabValue(value); navigate(`/workflows/${workflowId}/${value}`); }; const runButtonDisabled = useMemo(() => !workflow.hasContent, [workflow]); const [runButtonLoading, setRunButtonLoading] = useState(false); useEffect(() => { const running = workflow.lastRunStatus === WORKFLOW_RUN_STATUSES.PENDING || workflow.lastRunStatus === WORKFLOW_RUN_STATUSES.PROCESSING; setRunButtonLoading(running); }, [workflow.lastRunStatus]); const handleRunClick = () => { const { promise, resolve } = Promise.withResolvers(); if (workflow.hasDraft) { modal.confirm({ title: t("workflow.action.execute.modal.title"), content: t("workflow.action.execute.modal.content"), onOk: () => resolve(void 0), }); } else { resolve(void 0); } promise.then(async () => { try { setRunButtonLoading(true); await startWorkflowRun(workflow.id); message.info(t("workflow.action.execute.prompt")); } catch (err) { setRunButtonLoading(false); console.error(err); message.warning(t("common.text.operation_failed")); } }); }; const handleActiveClick = async () => { try { if (!workflow.enabled && !workflow.graphContent) { message.warning(t("workflow.action.enable.errmsg.unpublished")); return; } await workflowState.setEnabled(!workflow.enabled); } catch (err) { console.error(err); notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); } }; return (
({ value: key, label: {t(label)}, icon: ( {icon} ), }))} size="large" value={tabValue} onChange={handleTabChange} />
} >
); }; const WorkflowDetailBaseName = () => { const { t } = useTranslation(); const { notification } = App.useApp(); const { workflow, initialized, ...workflowStore } = useWorkflowStore(useZustandShallowSelector(["workflow", "initialized", "setName"])); const inputRef = useRef(null); const [editing, setEditing] = useState(false); const [value, setValue] = useState(""); useEffect(() => { setEditing(false); }, [workflow.id]); const handleEditClick = () => { if (!initialized) return; setEditing(true); setValue(workflow.name); setTimeout(() => { inputRef.current?.focus({ cursor: "all" }); }, 0); }; const handleValueChange = (value: string) => { setValue(value); }; const handleValueConfirm = async (value: string) => { value = value.trim(); if (!value || value === (workflow.name || "")) { setEditing(false); return; } setEditing(false); try { await workflowStore.setName(value); } catch (err) { notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); throw err; } }; return (

}> {workflow.name || t("workflow.detail.baseinfo.name.placeholder")}

); }; const WorkflowDetailBaseDescription = () => { const { t } = useTranslation(); const { notification } = App.useApp(); const { workflow, initialized, ...workflowStore } = useWorkflowStore(useZustandShallowSelector(["workflow", "initialized", "setDescription"])); const inputRef = useRef(null); const [editing, setEditing] = useState(false); const [value, setValue] = useState(""); useEffect(() => { setEditing(false); }, [workflow.id]); const handleEditClick = () => { if (!initialized) return; setEditing(true); setValue(workflow.description || ""); setTimeout(() => { inputRef.current?.focus({ cursor: "all" }); }, 0); }; const handleValueChange = (value: string) => { setValue(value); }; const handleValueConfirm = async (value: string) => { value = value.trim(); if (!value || value === (workflow.description || "")) { setEditing(false); return; } setEditing(false); try { await workflowStore.setDescription(value); } catch (err) { notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); throw err; } }; return (

{workflow.description || t("workflow.detail.baseinfo.description.placeholder")}

); }; export default WorkflowDetail; ================================================ FILE: ui/src/pages/workflows/WorkflowDetailDesign.tsx ================================================ import { useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { FlowLayoutDefault } from "@flowgram.ai/fixed-layout-editor"; import { IconArrowBackUp, IconDots, IconTransferIn, IconTransferOut } from "@tabler/icons-react"; import { useDeepCompareEffect } from "ahooks"; import { Alert, App, Button, Card, Dropdown, Result, Space, theme } from "antd"; import { debounce } from "radash"; import Show from "@/components/Show"; import { WorkflowDesigner, type WorkflowDesignerInstance, WorkflowNodeDrawer, WorkflowToolbar } from "@/components/workflow/designer"; import WorkflowGraphExportModal from "@/components/workflow/WorkflowGraphExportModal"; import WorkflowGraphImportModal from "@/components/workflow/WorkflowGraphImportModal"; import { useAppSettings, useZustandShallowSelector } from "@/hooks"; import { useWorkflowStore } from "@/stores/workflow"; import { unwrapErrMsg } from "@/utils/error"; const WorkflowDetailDesign = () => { const { t } = useTranslation(); const { token: themeToken } = theme.useToken(); const { message, modal, notification } = App.useApp(); const { appSettings: globalAppSettings } = useAppSettings(); const { workflow, ...workflowStore } = useWorkflowStore(useZustandShallowSelector(["workflow", "orchestrate", "publish", "rollback"])); const workflowRollbackDisabled = useMemo(() => !workflow.hasDraft || !workflow.hasContent, [workflow.hasDraft, workflow.hasContent]); const workflowPublishDisabled = useMemo(() => !workflow.hasDraft, [workflow.hasDraft]); const designerRef = useRef(null); const designerPending = useRef(false); // 保存中时阻止刷新画布 const [designerError, setDesignerError] = useState(); useDeepCompareEffect(() => { if (designerRef.current == null || designerRef.current.document.disposed) return; if (designerPending.current) return; try { const graph = workflow.graphDraft ?? { nodes: [] }; designerRef.current!.document.fromJSON(graph); setDesignerError(void 0); } catch (err) { console.error(err); setDesignerError(err); } }, [workflow.graphDraft]); const { drawerProps: designerNodeDrawerProps, ...designerNodeDrawer } = WorkflowNodeDrawer.useDrawer(); const handleDesignerDocumentChange = debounce({ delay: 300 }, async () => { if (designerRef.current == null || designerRef.current.document.disposed) return; designerPending.current = true; try { const graph = designerRef.current!.document.toJSON(); await workflowStore.orchestrate(graph); } catch (err) { console.error(err); notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); } finally { designerPending.current = false; } }); const handleRollbackClick = () => { modal.confirm({ title: t("workflow.detail.design.action.rollback.modal.title"), content: t("workflow.detail.design.action.rollback.modal.content"), onOk: async () => { try { await workflowStore.rollback(); message.success(t("common.text.operation_succeeded")); } catch (err) { console.error(err); notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); } }, }); }; const handlePublishClick = async () => { if (!(await designerRef.current!.validateAllNodes())) { message.warning(t("workflow.detail.design.uncompleted_design.alert")); return; } modal.confirm({ title: t("workflow.detail.design.action.publish.modal.title"), content: t("workflow.detail.design.action.publish.modal.content"), onOk: async () => { try { await workflowStore.publish(); message.success(t("common.text.operation_succeeded")); } catch (err) { console.error(err); notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); } }, }); }; const { modalProps: graphImportModalProps, ...graphImportModal } = WorkflowGraphImportModal.useModal(); const { modalProps: graphExportModalProps, ...graphExportModal } = WorkflowGraphExportModal.useModal(); const handleImportClick = async () => { graphImportModal.open().then(async (graph) => { const loadingKey = Math.random().toString(36).substring(0, 8); message.loading({ key: loadingKey, content: t("common.text.saving"), duration: 0 }); try { await workflowStore.orchestrate(graph); message.destroy(loadingKey); message.success(t("common.text.operation_succeeded")); } catch (err) { console.error(err); message.destroy(loadingKey); notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); } }); }; const handleExportClick = () => { graphExportModal.open({ data: workflow.graphDraft! }); }; return (
designerNodeDrawer.open(node)} >
{t("workflow.detail.design.unpublished_draft.alert")}
} type="warning" />
, onClick: handleRollbackClick, }, { type: "divider", }, { key: "import", label: t("workflow.detail.design.action.import.button"), icon: , onClick: handleImportClick, }, { key: "export", label: t("workflow.detail.design.action.export.button"), icon: , onClick: handleExportClick, }, ], }} trigger={["click"]} >
{!!designerError && (
)}
); }; export default WorkflowDetailDesign; ================================================ FILE: ui/src/pages/workflows/WorkflowDetailRuns.tsx ================================================ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { IconBox, IconBrowserShare, IconCertificate, IconDots, IconHistory, IconPlayerPause, IconTrash } from "@tabler/icons-react"; import { useRequest } from "ahooks"; import { App, Button, Dropdown, Skeleton, Table, type TableProps, theme } from "antd"; import dayjs from "dayjs"; import { ClientResponseError } from "pocketbase"; import { cancelRun as cancelWorkflowRun } from "@/api/workflows"; import Empty from "@/components/Empty"; import Show from "@/components/Show"; import Tips from "@/components/Tips"; import WorkflowRunDetailDrawer from "@/components/workflow/WorkflowRunDetailDrawer"; import WorkflowStatus from "@/components/workflow/WorkflowStatus"; import { WORKFLOW_TRIGGERS } from "@/domain/workflow"; import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun"; import { useAppSettings, useZustandShallowSelector } from "@/hooks"; import { get as getWorkflowRun, list as listWorkflowRuns, remove as removeWorkflowRun, subscribe as subscribeWorkflowRun } from "@/repository/workflowRun"; import { useWorkflowStore } from "@/stores/workflow"; import { unwrapErrMsg } from "@/utils/error"; const WorkflowDetailRuns = () => { const { t } = useTranslation(); const { token: themeToken } = theme.useToken(); const { modal, notification } = App.useApp(); const { appSettings: globalAppSettings } = useAppSettings(); const { workflow } = useWorkflowStore(useZustandShallowSelector(["workflow"])); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(globalAppSettings.defaultPerPage!); const [tableData, setTableData] = useState([]); const [tableTotal, setTableTotal] = useState(0); const [tableSelectedRowKeys, setTableSelectedRowKeys] = useState([]); const tableColumns: TableProps["columns"] = [ { key: "id", title: "ID", width: 160, render: (_, record) => {record.id}, }, { key: "status", title: t("workflow_run.props.status"), render: (_, record) => { return ; }, }, { key: "trigger", title: t("workflow_run.props.trigger"), ellipsis: true, render: (_, record) => { if (record.trigger === WORKFLOW_TRIGGERS.SCHEDULED) { return t("workflow_run.props.trigger.scheduled"); } else if (record.trigger === WORKFLOW_TRIGGERS.MANUAL) { return t("workflow_run.props.trigger.manual"); } return <>; }, }, { key: "startedAt", title: t("workflow_run.props.started_at"), ellipsis: true, render: (_, record) => { if (record.startedAt) { return dayjs(record.startedAt).format("YYYY-MM-DD HH:mm:ss"); } return <>; }, }, { key: "endedAt", title: t("workflow_run.props.ended_at"), ellipsis: true, render: (_, record) => { if (record.endedAt) { return dayjs(record.endedAt).format("YYYY-MM-DD HH:mm:ss"); } return <>; }, }, { key: "artifacts", title: t("workflow_run.props.artifacts"), width: 160, render: (_, record) => { if (record.outputs && record.outputs.length > 0) { const keys = new Set(); const icons: React.ReactNode[] = []; for (const output of record.outputs) { if (output.type === "ref" && output.value?.split("#")?.at(0) === "certificate") { const KEY = "certificate"; if (keys.has(KEY)) continue; keys.add(KEY); icons.push(); } else { const KEY = "other"; if (keys.has(KEY)) continue; keys.add(KEY); icons.push(); } } return
{icons}
; } return <>; }, }, { key: "$action", align: "end", fixed: "right", width: 64, render: (_, record) => { const cancelDisabled = !([WORKFLOW_RUN_STATUSES.PENDING, WORKFLOW_RUN_STATUSES.PROCESSING] as string[]).includes(record.status); const deleteDisabled = !cancelDisabled; return ( ), onClick: () => { handleRecordDetailClick(record); }, }, { key: "cancel", label: {t("workflow_run.action.cancel.menu")}, icon: ( ), disabled: cancelDisabled, onClick: () => { handleRecordCancelClick(record); }, }, { type: "divider", }, { key: "delete", label: t("workflow_run.action.delete.menu"), icon: ( ), danger: true, disabled: deleteDisabled, onClick: () => { handleRecordDeleteClick(record); }, }, ], }} trigger={["click"]} > ); }; export default WorkflowDetailRuns; ================================================ FILE: ui/src/pages/workflows/WorkflowList.tsx ================================================ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate, useSearchParams } from "react-router-dom"; import { IconCirclePlus, IconCopy, IconDots, IconEdit, IconHierarchy3, IconPlayerPlay, IconPlus, IconReload, IconTrash } from "@tabler/icons-react"; import { useControllableValue, useRequest } from "ahooks"; import { App, Button, Dropdown, Form, Input, Segmented, Skeleton, Switch, Table, type TableProps, Typography, theme } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import dayjs from "dayjs"; import { ClientResponseError } from "pocketbase"; import { z } from "zod"; import { startRun as startWorkflowRun } from "@/api/workflows"; import DrawerForm from "@/components/DrawerForm"; import Empty from "@/components/Empty"; import Show from "@/components/Show"; import WorkflowStatus from "@/components/workflow/WorkflowStatus"; import { WORKFLOW_TRIGGERS, type WorkflowModel, duplicateNodes } from "@/domain/workflow"; import { useAntdForm, useAppSettings } from "@/hooks"; import { get as getWorkflow, list as listWorkflows, remove as removeWorkflow, save as saveWorkflow } from "@/repository/workflow"; import { unwrapErrMsg } from "@/utils/error"; const WorkflowList = () => { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const { t } = useTranslation(); const { token: themeToken } = theme.useToken(); const { message, modal, notification } = App.useApp(); const { appSettings: globalAppSettings } = useAppSettings(); const [filters, setFilters] = useState>(() => { return { keyword: searchParams.get("keyword"), state: searchParams.get("state"), }; }); const [sorter, setSorter] = useState["onChange"]>>[2]>>(() => { return {}; }); const [page, setPage] = useState(() => parseInt(+searchParams.get("page")! + "") || 1); const [pageSize, setPageSize] = useState(() => parseInt(+searchParams.get("perPage")! + "") || globalAppSettings.defaultPerPage!); const [tableData, setTableData] = useState([]); const [tableTotal, setTableTotal] = useState(0); const [tableSelectedRowKeys, setTableSelectedRowKeys] = useState([]); const tableColumns: TableProps["columns"] = [ { key: "name", title: t("workflow.props.name"), render: (_, record) => (
{record.name || "\u00A0"} {record.description || "\u00A0"}
), }, { key: "trigger", title: t("workflow.props.trigger"), render: (_, record) => { const trigger = record.trigger; if (!trigger) { return "-"; } else if (trigger === WORKFLOW_TRIGGERS.MANUAL) { return {t("workflow.props.trigger.manual")}; } else if (trigger === WORKFLOW_TRIGGERS.SCHEDULED) { return (
{t("workflow.props.trigger.scheduled")} {record.triggerCron || "\u00A0"}
); } }, }, { key: "state", title: t("workflow.props.state"), defaultFilteredValue: searchParams.has("state") ? [searchParams.get("state") as string] : void 0, render: (_, record) => { return ( { handleRecordActiveChange(record); }} /> ); }, onCell: () => { return { onClick: (e) => { e.stopPropagation(); }, }; }, }, { key: "lastRun", title: t("workflow.props.last_run_at"), sorter: true, sortOrder: sorter.columnKey === "lastRun" ? sorter.order : void 0, render: (_, record) => { const { lastRunStatus, lastRunTime } = record; if (!lastRunStatus) { return <>; } else { return ( {lastRunTime ? dayjs(lastRunTime).format("YYYY-MM-DD HH:mm:ss") : ""} ); } }, }, { key: "createdAt", title: t("workflow.props.created_at"), ellipsis: true, render: (_, record) => { return dayjs(record.created!).format("YYYY-MM-DD HH:mm:ss"); }, }, { key: "$action", align: "end", fixed: "right", width: 64, render: (_, record) => ( ), onClick: () => { handleRecordDetailClick(record); }, }, { key: "duplicate", label: t("workflow.action.duplicate.menu"), icon: ( ), onClick: () => { handleRecordDuplicateClick(record); }, }, { key: "execute", label: t("workflow.action.execute.menu"), icon: ( ), disabled: !record.hasContent, onClick: () => { handleRecordExecuteClick(record); }, }, { type: "divider", }, { key: "delete", label: t("workflow.action.delete.menu"), danger: true, icon: ( ), onClick: () => { handleRecordDeleteClick(record); }, }, ], }} trigger={["click"]} >
columns={tableColumns} dataSource={tableData} loading={loading} locale={{ emptyText: loading ? ( ) : ( } extra={ loadError ? ( ) : ( ) } /> ), }} pagination={{ current: page, pageSize: pageSize, total: tableTotal, showSizeChanger: true, onChange: handlePaginationChange, onShowSizeChange: handlePaginationChange, }} rowClassName="cursor-pointer" rowKey={(record) => record.id} rowSelection={tableRowSelection} scroll={{ x: "max(100%, 960px)" }} onChange={(_, __, sorter) => { setSorter(Array.isArray(sorter) ? sorter[0] : sorter); }} onRow={(record) => ({ onClick: () => { handleRecordDetailClick(record); }, })} /> 0}>
setDuplicateDrawerOpen(false)} onOpenChange={(open) => setDuplicateDrawerOpen(open)} onSubmit={handleDuplicateDrawerSubmit} />
); }; const InternalDuplicateDrawer = ({ data, trigger, onSubmit, ...props }: { afterClose?: () => void; data?: Nullish; open: boolean; trigger?: React.ReactNode; onOpenChange?: (open: boolean) => void; onSubmit?: (record: Nullish) => void; }) => { const { t } = useTranslation(); const [open, setOpen] = useControllableValue(props, { valuePropName: "open", defaultValuePropName: "defaultOpen", trigger: "onOpenChange", }); const afterClose = () => { formInst.resetFields(); props.afterClose?.(); }; const formSchema = z.object({ name: z.string().nonempty(t("workflow.detail.baseinfo.name.placeholder")), description: z.string().nullish(), }); const formRule = createSchemaFieldRule(formSchema); const { form: formInst, formProps } = useAntdForm>({ name: "viewWorkflowList_InternalDuplicateDrawer", initialValues: data, }); const handleFormFinish = async (values: z.infer) => { await onSubmit?.(values); setOpen(false); }; return ( !open && afterClose?.() }} form={formInst} layout="vertical" okText={t("common.button.create")} open={open} preserve={false} title={t("workflow.action.create.modal.title")} trigger={trigger} validateTrigger="onSubmit" onFinish={handleFormFinish} onOpenChange={props.onOpenChange} > ); }; export default WorkflowList; ================================================ FILE: ui/src/pages/workflows/WorkflowNew.tsx ================================================ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { IconArrowRight, IconSquarePlus2, IconUpload } from "@tabler/icons-react"; import { App, Button, Card, Spin, Typography } from "antd"; import Show from "@/components/Show"; import WorkflowGraphImportModal from "@/components/workflow/WorkflowGraphImportModal"; import { WORKFLOW_NODE_TYPES, type WorkflowModel, type WorkflowNodeConfigForBizDeploy, type WorkflowNodeConfigForBizNotify, type WorkflowNodeConfigForBranchBlock, newNode, } from "@/domain/workflow"; import { save as saveWorkflow } from "@/repository/workflow"; import { unwrapErrMsg } from "@/utils/error"; const TEMPLATE_KEY_BLANK = "blank" as const; const TEMPLATE_KEY_STANDARD = "standard" as const; const TEMPLATE_KEY_CERTTEST = "certtest" as const; type TemplateKeys = typeof TEMPLATE_KEY_BLANK | typeof TEMPLATE_KEY_CERTTEST | typeof TEMPLATE_KEY_STANDARD; const WorkflowNew = () => { const navigate = useNavigate(); const { i18n, t } = useTranslation(); const { notification } = App.useApp(); const templates = [ { key: TEMPLATE_KEY_STANDARD, name: t("workflow.new.templates.template.standard.title"), description: t("workflow.new.templates.template.standard.description"), image: "/imgs/workflow/tpl-standard.png", }, { key: TEMPLATE_KEY_CERTTEST, name: t("workflow.new.templates.template.certtest.title"), description: t("workflow.new.templates.template.certtest.description"), image: "/imgs/workflow/tpl-certtest.png", }, ]; const [templateSelectKey, setTemplateSelectKey] = useState(); const [templatePending, setTemplatePending] = useState(false); const renderTemplateCard = ({ key, name, description, image }: { key: TemplateKeys; name: string; description: string; image: string }) => { return ( } hoverable onClick={() => handleTemplateClick(key)} >
{name}
}>
} description={description} />
); }; const { modalProps: workflowImportModalProps, ...workflowImportModal } = WorkflowGraphImportModal.useModal(); const handleTemplateClick = async (key: TemplateKeys) => { if (templatePending) return; setTemplateSelectKey(key); setTemplatePending(true); try { let workflow = {} as WorkflowModel; workflow.name = t("workflow.new.templates.default_name"); workflow.description = t("workflow.new.templates.default_description"); workflow.graphDraft = { nodes: [] }; workflow.hasDraft = true; switch (key) { case TEMPLATE_KEY_BLANK: { const startNode = newNode(WORKFLOW_NODE_TYPES.START, { i18n: i18n }); const endNode = newNode(WORKFLOW_NODE_TYPES.END, { i18n: i18n }); workflow.graphDraft!.nodes = [startNode, endNode]; } break; case TEMPLATE_KEY_STANDARD: { const startNode = newNode(WORKFLOW_NODE_TYPES.START, { i18n: i18n }); const tryCatchNode = newNode(WORKFLOW_NODE_TYPES.TRYCATCH, { i18n: i18n }); const applyNode = newNode(WORKFLOW_NODE_TYPES.BIZ_APPLY, { i18n: i18n }); const deployNode = newNode(WORKFLOW_NODE_TYPES.BIZ_DEPLOY, { i18n: i18n }); const notifyOnFailureNode = newNode(WORKFLOW_NODE_TYPES.BIZ_NOTIFY, { i18n: i18n }); const endNode = newNode(WORKFLOW_NODE_TYPES.END, { i18n: i18n }); deployNode.data.config = { ...deployNode.data.config, certificateOutputNodeId: applyNode.id, } as WorkflowNodeConfigForBizDeploy; notifyOnFailureNode.data.config = { ...notifyOnFailureNode.data.config, subject: "[Certimate] Workflow Failure Alert!", message: 'Your workflow "{{ $workflow.name }}" run has failed. Please check the details.', } as WorkflowNodeConfigForBizNotify; tryCatchNode.blocks!.at(0)!.blocks ??= []; tryCatchNode.blocks!.at(0)!.blocks!.push(applyNode, deployNode); tryCatchNode.blocks!.at(1)!.blocks ??= []; tryCatchNode.blocks!.at(1)!.blocks!.unshift(notifyOnFailureNode); workflow.graphDraft!.nodes = [startNode, tryCatchNode, endNode]; } break; case TEMPLATE_KEY_CERTTEST: { const startNode = newNode(WORKFLOW_NODE_TYPES.START, { i18n: i18n }); const tryCatchNode = newNode(WORKFLOW_NODE_TYPES.TRYCATCH, { i18n: i18n }); const monitorNode = newNode(WORKFLOW_NODE_TYPES.BIZ_MONITOR, { i18n: i18n }); const conditionNode = newNode(WORKFLOW_NODE_TYPES.CONDITION, { i18n: i18n }); const notifyOnExpiringSoonNode = newNode(WORKFLOW_NODE_TYPES.BIZ_NOTIFY, { i18n: i18n }); const notifyOnExpiredNode = newNode(WORKFLOW_NODE_TYPES.BIZ_NOTIFY, { i18n: i18n }); const notifyOnFailureNode = newNode(WORKFLOW_NODE_TYPES.BIZ_NOTIFY, { i18n: i18n }); const endNode = newNode(WORKFLOW_NODE_TYPES.END, { i18n: i18n }); notifyOnExpiringSoonNode.data.config = { ...notifyOnExpiringSoonNode.data.config, subject: "[Certimate] Certificate Expiry Alert!", message: "The certificate which you are monitoring will be expiring soon. Please pay attention to your website. \r\nDomains: {{ $certificate.subjectAltNames }} \r\nExpiration: {{ $certificate.notAfter }}({{ $certificate.daysLeft }} days left)", } as WorkflowNodeConfigForBizNotify; notifyOnExpiredNode.data.config = { ...notifyOnExpiredNode.data.config, subject: "[Certimate] Certificate Expiry Alert!", message: "The certificate which you are monitoring has already expired. Please pay attention to your website. \r\nDomains: {{ $certificate.subjectAltNames }} \r\nExpiration: {{ $certificate.notAfter }}", } as WorkflowNodeConfigForBizNotify; notifyOnFailureNode.data.config = { ...notifyOnFailureNode.data.config, subject: "[Certimate] Workflow Failure Alert!", message: 'Your workflow "{{ $workflow.name }}" run has failed. Please check the details.', } as WorkflowNodeConfigForBizNotify; tryCatchNode.blocks!.at(0)!.blocks ??= []; tryCatchNode.blocks!.at(0)!.blocks!.push(monitorNode, conditionNode); tryCatchNode.blocks!.at(1)!.blocks ??= []; tryCatchNode.blocks!.at(1)!.blocks!.unshift(notifyOnFailureNode); conditionNode.blocks!.at(0)!.data.name = t("workflow_node.condition.default_name.template_certtest_on_expiring_soon"); conditionNode.blocks!.at(0)!.data.config = { ...conditionNode.blocks!.at(0)!.data.config, expression: { left: { left: { selector: { id: monitorNode.id, name: "certificate.validity", type: "boolean", }, type: "var", }, operator: "eq", right: { type: "const", value: "true", valueType: "boolean", }, type: "comparison", }, operator: "and", right: { left: { selector: { id: monitorNode.id, name: "certificate.daysLeft", type: "number", }, type: "var", }, operator: "lte", right: { type: "const", value: "30", valueType: "number", }, type: "comparison", }, type: "logical", }, } as WorkflowNodeConfigForBranchBlock; conditionNode.blocks!.at(0)!.blocks ??= []; conditionNode.blocks!.at(0)!.blocks!.push(notifyOnExpiringSoonNode); conditionNode.blocks!.at(1)!.data.name = t("workflow_node.condition.default_name.template_certtest_on_expired"); conditionNode.blocks!.at(1)!.data.config = { ...conditionNode.blocks!.at(1)!.data.config, expression: { left: { selector: { id: monitorNode.id, name: "certificate.validity", type: "boolean", }, type: "var", }, operator: "eq", right: { type: "const", value: "false", valueType: "boolean", }, type: "comparison", }, } as WorkflowNodeConfigForBranchBlock; conditionNode.blocks!.at(1)!.blocks ??= []; conditionNode.blocks!.at(1)!.blocks!.push(notifyOnExpiredNode); workflow.graphDraft!.nodes = [startNode, tryCatchNode, endNode]; } break; default: throw "Invalid value of `templateSelectKey`"; } workflow = await saveWorkflow(workflow); navigate(`/workflows/${workflow.id}`, { replace: true }); } catch (err) { notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); throw err; } finally { setTemplatePending(false); setTemplateSelectKey(void 0); } }; const handleImportClick = async () => { if (templatePending) return; workflowImportModal.open().then(async (graph) => { setTemplatePending(true); try { let workflow = {} as WorkflowModel; workflow.name = t("workflow.new.templates.default_name"); workflow.description = t("workflow.new.templates.default_description"); workflow.graphDraft = graph; workflow.hasDraft = true; workflow = await saveWorkflow(workflow); navigate(`/workflows/${workflow.id}`, { replace: true }); } catch (err) { notification.error({ title: t("common.text.request_error"), description: unwrapErrMsg(err) }); throw err; } finally { setTemplatePending(false); } }); }; return (

{t("workflow.new.title")}

{t("workflow.new.subtitle")}

{t("workflow.new.templates.title")}

{t("workflow.new.templates.subtitle")}
{templates.map((template) => renderTemplateCard(template))}
); }; export default WorkflowNew; ================================================ FILE: ui/src/repository/_pocketbase.ts ================================================ import PocketBase from "pocketbase"; let pb: PocketBase; export const getPocketBase = () => { if (pb) return pb; pb = new PocketBase("/"); pb.afterSend = (res, data) => { if ((res.status === 401 || res.status === 403) && pb.authStore?.isValid) { pb.authStore.clear(); location.reload(); } return data; }; 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"; export const COLLECTION_NAME_WORKFLOW_LOG = "workflow_logs"; ================================================ FILE: ui/src/repository/access.ts ================================================ import dayjs from "dayjs"; import { type AccessModel } from "@/domain/access"; import { COLLECTION_NAME_ACCESS, getPocketBase } from "./_pocketbase"; const _commonFields = ["id", "name", "provider", "reserve", "created", "updated", "deleted"]; export const list = async () => { const list = await getPocketBase() .collection(COLLECTION_NAME_ACCESS) .getFullList({ batch: 65535, fields: [..._commonFields].join(","), filter: "deleted=null", sort: "-created", requestKey: null, }); return { totalItems: list.length, items: list, }; }; export const get = async (id: string) => { return await getPocketBase().collection(COLLECTION_NAME_ACCESS).getOne(id, { requestKey: null, }); }; export const save = async (record: MaybeModelRecord) => { if (record.id) { return await getPocketBase().collection(COLLECTION_NAME_ACCESS).update(record.id, record); } return await getPocketBase().collection(COLLECTION_NAME_ACCESS).create(record); }; export const remove = async (record: MaybeModelRecordWithId | MaybeModelRecordWithId[]) => { const pb = getPocketBase(); const deletedAt = dayjs.utc().format("YYYY-MM-DD HH:mm:ss"); if (Array.isArray(record)) { const batch = pb.createBatch(); for (const item of record) { batch.collection(COLLECTION_NAME_ACCESS).update(item.id, { deleted: deletedAt }); } const res = await batch.send(); return res.every((e) => e.status >= 200 && e.status < 400); } else { await pb.collection(COLLECTION_NAME_ACCESS).update(record.id!, { deleted: deletedAt }); return true; } }; ================================================ FILE: ui/src/repository/admin.ts ================================================ import { COLLECTION_NAME_ADMIN, getPocketBase } from "./_pocketbase"; export const authWithPassword = (username: string, password: string) => { return getPocketBase().collection(COLLECTION_NAME_ADMIN).authWithPassword(username, password); }; export const getAuthStore = () => { return getPocketBase().authStore; }; export const save = (data: { email: string } | { password: string; passwordConfirm: string }) => { return getPocketBase() .collection(COLLECTION_NAME_ADMIN) .update(getAuthStore().record?.id || "", data); }; ================================================ FILE: ui/src/repository/certificate.ts ================================================ import dayjs from "dayjs"; import { type CertificateModel } from "@/domain/certificate"; import { COLLECTION_NAME_CERTIFICATE, getPocketBase } from "./_pocketbase"; const _commonFields = [ "id", "source", "subjectAltNames", "serialNumber", "issuerOrg", "keyAlgorithm", "validityNotBefore", "validityNotAfter", "validityInterval", "isRenewed", "isRevoked", "workflowRef", "created", "updated", "deleted", ]; const _expandFields = ["expand.workflowRef.id", "expand.workflowRef.name", "expand.workflowRef.description"]; export const list = async ({ keyword, state, stateThreshold, sort = "-created", page = 1, perPage = 10, }: { keyword?: string; state?: "expiringSoon" | "expired"; stateThreshold?: number; sort?: string; page?: number; perPage?: number; }) => { const pb = getPocketBase(); const filters: string[] = ["deleted=null"]; if (keyword) { filters.push(pb.filter("(id={:keyword} || serialNumber={:keyword} || subjectAltNames~{:keyword})", { keyword: keyword })); } if (state === "expiringSoon") { filters.push(pb.filter("validityNotAfter<={:expiredAt}", { expiredAt: dayjs().add(stateThreshold!, "d").toDate() })); filters.push(pb.filter("validityNotAfter>@now")); filters.push(pb.filter("isRevoked=0")); } else if (state === "expired") { filters.push(pb.filter("validityNotAfter<=@now")); } return pb.collection(COLLECTION_NAME_CERTIFICATE).getList(page, perPage, { expand: ["workflowRef"].join(","), fields: [..._commonFields, ..._expandFields].join(","), filter: filters.join(" && "), sort: sort || "-created", requestKey: null, }); }; export const listByWorkflowRunId = async (workflowRunId: string) => { const pb = getPocketBase(); const list = await pb.collection(COLLECTION_NAME_CERTIFICATE).getFullList({ batch: 65535, fields: [..._commonFields, ..._expandFields, "certificate", "privateKey"].join(","), filter: pb.filter("workflowRunRef={:workflowRunId}", { workflowRunId }), sort: "created", requestKey: null, }); return { totalItems: list.length, items: list, }; }; export const get = async (id: string) => { return await getPocketBase() .collection(COLLECTION_NAME_CERTIFICATE) .getOne(id, { expand: ["workflowRef"].join(","), fields: ["*", ..._expandFields].join(","), requestKey: null, }); }; export const remove = async (record: MaybeModelRecordWithId | MaybeModelRecordWithId[]) => { const pb = getPocketBase(); const deletedAt = dayjs.utc().format("YYYY-MM-DD HH:mm:ss"); if (Array.isArray(record)) { const batch = pb.createBatch(); for (const item of record) { batch.collection(COLLECTION_NAME_CERTIFICATE).update(item.id, { deleted: deletedAt }); } const res = await batch.send(); return res.every((e) => e.status >= 200 && e.status < 400); } else { await pb.collection(COLLECTION_NAME_CERTIFICATE).update(record.id!, { deleted: deletedAt }); return true; } }; ================================================ FILE: ui/src/repository/settings.ts ================================================ import { ClientResponseError } from "pocketbase"; import { CA_PROVIDERS } from "@/domain/provider"; import { type EmailsSettingsContent, type NotifyTemplateContent, type PersistenceSettingsContent, SETTINGS_NAMES, type SSLProviderSettingsContent, type ScriptTemplateContent, type SettingsModel, type SettingsNames, } from "@/domain/settings"; import { COLLECTION_NAME_SETTINGS, getPocketBase } from "./_pocketbase"; interface SettingsContentMap { [SETTINGS_NAMES.EMAILS]: EmailsSettingsContent; [SETTINGS_NAMES.NOTIFY_TEMPLATE]: NotifyTemplateContent; [SETTINGS_NAMES.SCRIPT_TEMPLATE]: ScriptTemplateContent; [SETTINGS_NAMES.SSL_PROVIDER]: SSLProviderSettingsContent; [SETTINGS_NAMES.PERSISTENCE]: PersistenceSettingsContent; } export const get = async >( name: K ): Promise : SettingsModel> => { let resp: K extends keyof SettingsContentMap ? SettingsModel : SettingsModel; try { resp = await getPocketBase().collection(COLLECTION_NAME_SETTINGS).getFirstListItem(`name='${name}'`, { requestKey: null, }); return resp; } catch (err) { if (err instanceof ClientResponseError && err.status === 404) { resp = { name: name, content: {}, } as unknown as typeof resp; } else { throw err; } } // 兜底设置一些默认值(需确保与后端默认值保持一致),防止视图层空指针 switch (name) { case SETTINGS_NAMES.EMAILS: { resp.content ??= {}; (resp.content as EmailsSettingsContent).emails ??= []; } break; case SETTINGS_NAMES.NOTIFY_TEMPLATE: { resp.content ??= {}; (resp.content as NotifyTemplateContent).templates ??= []; } break; case SETTINGS_NAMES.SCRIPT_TEMPLATE: { resp.content ??= {}; (resp.content as ScriptTemplateContent).templates ??= []; } break; case SETTINGS_NAMES.SSL_PROVIDER: { resp.content ??= {}; (resp.content as SSLProviderSettingsContent).provider ??= CA_PROVIDERS.LETSENCRYPT; } break; case SETTINGS_NAMES.PERSISTENCE: { resp.content ??= {}; (resp.content as PersistenceSettingsContent).certificatesWarningDaysBeforeExpire ??= 21; (resp.content as PersistenceSettingsContent).certificatesRetentionMaxDays ??= 0; (resp.content as PersistenceSettingsContent).workflowRunsRetentionMaxDays ??= 0; } break; } return resp; }; export const save = async >(record: MaybeModelRecordWithId>) => { if (record.id) { return await getPocketBase().collection(COLLECTION_NAME_SETTINGS).update>(record.id, record); } return await getPocketBase().collection(COLLECTION_NAME_SETTINGS).create>(record); }; ================================================ FILE: ui/src/repository/system.ts ================================================ import { getPocketBase } from "./_pocketbase"; export const listCronJobs = () => { return getPocketBase() .crons.getFullList({ requestKey: null, }) .then((res) => { const jobs = res .filter((job) => !job.id.startsWith("__pb")) .map((job) => { return { id: job.id, cron: job.expression, }; }); return { items: jobs, }; }); }; export type ListLogsRequest = { page?: number; perPage?: number; }; export const listLogs = (request: ListLogsRequest) => { const page = request.page || 1; const perPage = request.perPage || 10; return getPocketBase() .logs.getList(page, perPage, { filter: 'data.type!="request"', sort: "-@rowid", skipTotal: true, requestKey: null, }) .then((res) => { return { items: res.items, }; }); }; ================================================ FILE: ui/src/repository/workflow.ts ================================================ import { type RecordSubscription } from "pocketbase"; import { type WorkflowModel } from "@/domain/workflow"; import { COLLECTION_NAME_WORKFLOW, getPocketBase } from "./_pocketbase"; const _commonFields = [ "id", "name", "description", "trigger", "triggerCron", "enabled", "hasDraft", "hasContent", "lastRunRef", "lastRunStatus", "lastRunTime", "created", "updated", "deleted", ]; const _expandFields = [ "expand.lastRunRef.id", "expand.lastRunRef.status", "expand.lastRunRef.trigger", "expand.lastRunRef.startedAt", "expand.lastRunRef.endedAt", "expand.lastRunRef.error", ]; export const list = async ({ keyword, enabled, sort = "-created", page = 1, perPage = 10, expand = false, }: { keyword?: string; enabled?: boolean; sort?: string; page?: number; perPage?: number; expand?: boolean; }) => { const pb = getPocketBase(); const filters: string[] = []; if (keyword) { filters.push(pb.filter("(id={:keyword} || name~{:keyword})", { keyword: keyword })); } if (enabled != null) { filters.push(pb.filter("enabled={:enabled}", { enabled: enabled })); } return await pb.collection(COLLECTION_NAME_WORKFLOW).getList(page, perPage, { expand: expand ? ["lastRunRef"].join(",") : void 0, fields: [..._commonFields, ..._expandFields].join(","), filter: filters.join(" && "), sort: sort || "-created", requestKey: null, }); }; export const get = async (id: string) => { return await getPocketBase() .collection(COLLECTION_NAME_WORKFLOW) .getOne(id, { expand: ["lastRunRef"].join(","), fields: ["*", ..._expandFields].join(","), requestKey: null, }); }; export const save = async (record: MaybeModelRecord) => { if (record.id) { return await getPocketBase() .collection(COLLECTION_NAME_WORKFLOW) .update(record.id as string, record); } return await getPocketBase().collection(COLLECTION_NAME_WORKFLOW).create(record); }; export const remove = async (record: MaybeModelRecordWithId | MaybeModelRecordWithId[]) => { const pb = getPocketBase(); if (Array.isArray(record)) { const batch = pb.createBatch(); for (const item of record) { batch.collection(COLLECTION_NAME_WORKFLOW).delete(item.id); } const res = await batch.send(); return res.every((e) => e.status >= 200 && e.status < 400); } else { return await pb.collection(COLLECTION_NAME_WORKFLOW).delete(record.id); } }; export const subscribe = async (id: string, cb: (e: RecordSubscription) => void) => { return getPocketBase().collection(COLLECTION_NAME_WORKFLOW).subscribe(id, cb); }; export const unsubscribe = async (id: string) => { return getPocketBase().collection(COLLECTION_NAME_WORKFLOW).unsubscribe(id); }; ================================================ FILE: ui/src/repository/workflowLog.ts ================================================ import { type WorkflowLogModel } from "@/domain/workflowLog"; import { COLLECTION_NAME_WORKFLOW_LOG, getPocketBase } from "./_pocketbase"; export const listByWorkflowRunId = async (workflowRunId: string) => { const pb = getPocketBase(); const list = await pb.collection(COLLECTION_NAME_WORKFLOW_LOG).getFullList({ batch: 65535, filter: pb.filter("runRef={:workflowRunId}", { workflowRunId }), sort: "timestamp", requestKey: null, }); return { totalItems: list.length, items: list, }; }; ================================================ FILE: ui/src/repository/workflowRun.ts ================================================ import { type RecordSubscription } from "pocketbase"; import { type WorkflowRunModel } from "@/domain/workflowRun"; import { COLLECTION_NAME_WORKFLOW_OUTPUT, COLLECTION_NAME_WORKFLOW_RUN, getPocketBase } from "./_pocketbase"; const _commonFields = ["id", "status", "trigger", "startedAt", "endedAt", "error", "created", "updated", "deleted"]; const _expandFields = ["expand.workflowRef.id", "expand.workflowRef.name", "expand.workflowRef.description"]; export const list = async ({ workflowId, page = 1, perPage = 10, expand = false, }: { workflowId?: string; page?: number; perPage?: number; expand?: boolean; }) => { const pb = getPocketBase(); const filters: string[] = []; if (workflowId) { filters.push(pb.filter("workflowRef={:workflowId}", { workflowId: workflowId })); } const list = await pb.collection(COLLECTION_NAME_WORKFLOW_RUN).getList(page, perPage, { expand: expand ? ["workflowRef"].join(",") : void 0, fields: [..._commonFields, ..._expandFields].join(","), filter: filters.join(" && "), sort: "-created", requestKey: null, }); await enrichOutputs(list.items); return list; }; export const get = async (id: string) => { const record = await getPocketBase() .collection(COLLECTION_NAME_WORKFLOW_RUN) .getOne(id, { expand: ["workflowRef"].join(","), fields: ["*", ..._expandFields].join(","), requestKey: null, }); await enrichOutputs(record); return record; }; export const remove = async (record: MaybeModelRecordWithId | MaybeModelRecordWithId[]) => { const pb = getPocketBase(); if (Array.isArray(record)) { const batch = pb.createBatch(); for (const item of record) { batch.collection(COLLECTION_NAME_WORKFLOW_RUN).delete(item.id); } const res = await batch.send(); return res.every((e) => e.status >= 200 && e.status < 400); } else { await pb.collection(COLLECTION_NAME_WORKFLOW_RUN).delete(record.id!); return true; } }; 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); }; const enrichOutputs = async (records: WorkflowRunModel | WorkflowRunModel[]) => { if (!Array.isArray(records)) { records = [records]; } const runIds = Array.from(new Set(records.map((e) => e.id))); if (runIds.length === 0) { return; } const pb = getPocketBase(); const list = await pb.collection(COLLECTION_NAME_WORKFLOW_OUTPUT).getFullList({ batch: 65535, fields: ["id", "runRef", "outputs"].join(","), filter: "(" + runIds.map((runId) => pb.filter("runRef={:runId}", { runId })).join(" || ") + ") && outputs!=null", sort: "created", requestKey: null, }); for (const record of records) { const outputs = list .filter((e) => e.runRef === record.id) .map((e) => e.outputs) .flat(); record.outputs = outputs; } }; ================================================ FILE: ui/src/routers/index.tsx ================================================ import { createHashRouter } from "react-router-dom"; import AccessList from "@/pages/accesses/AccessList"; import AccessNew from "@/pages/accesses/AccessNew"; import AuthLayout from "@/pages/AuthLayout"; import CertificateList from "@/pages/certificates/CertificateList"; import ConsoleLayout from "@/pages/ConsoleLayout"; import Dashboard from "@/pages/dashboard/Dashboard"; import ErrorLayout from "@/pages/ErrorLayout"; import Login from "@/pages/login/Login"; import PresetList from "@/pages/presets/PresetList"; import Settings from "@/pages/settings/Settings"; import SettingsAbout from "@/pages/settings/SettingsAbout"; import SettingsAccount from "@/pages/settings/SettingsAccount"; import SettingsAppearance from "@/pages/settings/SettingsAppearance"; import SettingsDiagnostics from "@/pages/settings/SettingsDiagnostics"; import SettingsPersistence from "@/pages/settings/SettingsPersistence"; import SettingsSSLProvider from "@/pages/settings/SettingsSSLProvider"; import WorkflowDetail from "@/pages/workflows/WorkflowDetail"; import WorkflowDetailDesign from "@/pages/workflows/WorkflowDetailDesign"; import WorkflowDetailRuns from "@/pages/workflows/WorkflowDetailRuns"; import WorkflowList from "@/pages/workflows/WorkflowList"; import WorkflowNew from "@/pages/workflows/WorkflowNew"; export const router = createHashRouter([ { path: "/", element: , children: [ { path: "/", element: , }, { path: "/accesses", element: , }, { path: "/accesses/new", element: , }, { path: "/certificates", element: , }, { path: "/workflows", element: , }, { path: "/workflows/new", element: , }, { path: "/workflows/:id", element: , children: [ { path: "/workflows/:id/design", element: , }, { path: "/workflows/:id/runs", element: , }, ], }, { path: "/presets", element: , }, { path: "/settings", element: , children: [ { path: "/settings/account", element: , }, { path: "/settings/appearance", element: , }, { path: "/settings/ssl-provider", element: , }, { path: "/settings/persistence", element: , }, { path: "/settings/diagnostics", element: , }, { path: "/settings/about", element: , }, ], }, ], }, { path: "/login", element: , children: [ { path: "/login", element: , }, ], }, { path: "*", element: (

404

This page could not be found.

), }, ]); ================================================ FILE: ui/src/stores/access/index.ts ================================================ import { produce } from "immer"; import { create } from "zustand"; import { type AccessModel } from "@/domain/access"; import { list as listAccesses, remove as removeAccess, save as saveAccess } from "@/repository/access"; import { type AccessesState, type AccessesStore } from "./types"; export const useAccessesStore = create((set, get) => { let fetcher: Promise | null = null; // 防止多次重复请求 return { accesses: [], loading: false, loadedAtOnce: false, fetchAccesses: async (refresh = true) => { if (!refresh) { if (get().loadedAtOnce) { return get().accesses; } } fetcher ??= listAccesses().then((res) => res.items); try { set({ loading: true }); const accesses = await fetcher; set({ accesses: accesses ?? [], loadedAtOnce: true }); } finally { fetcher = null; set({ loading: false }); } return get().accesses; }, createAccess: async (access) => { const record = await saveAccess(access); set( produce((state: AccessesState) => { state.accesses.unshift(record); }) ); return record as AccessModel; }, updateAccess: async (access) => { const record = await saveAccess(access); set( produce((state: AccessesState) => { const index = state.accesses.findIndex((e) => e.id === record.id); if (index !== -1) { state.accesses[index] = record; } }) ); return record as AccessModel; }, deleteAccess: async (access) => { await removeAccess(access); if (Array.isArray(access)) { set( produce((state: AccessesState) => { state.accesses = state.accesses.filter((e) => !access.some((item) => item.id === e.id)); }) ); } else { set( produce((state: AccessesState) => { state.accesses = state.accesses.filter((e) => e.id !== access.id); }) ); } return access as AccessModel; }, }; }); ================================================ FILE: ui/src/stores/access/types.ts ================================================ import { type AccessModel } from "@/domain/access"; export interface AccessesState { accesses: AccessModel[]; loading: boolean; loadedAtOnce: boolean; } export interface AccessesActions { fetchAccesses: (refresh?: boolean) => Promise; createAccess: (access: MaybeModelRecord) => Promise; updateAccess: (access: MaybeModelRecordWithId) => Promise; deleteAccess: (access: MaybeModelRecordWithId | MaybeModelRecordWithId[]) => Promise; } export interface AccessesStore extends AccessesState, AccessesActions {} ================================================ FILE: ui/src/stores/settings/contact/index.ts ================================================ import { produce } from "immer"; import { create } from "zustand"; import { type EmailsSettingsContent, SETTINGS_NAMES, type SettingsModel } from "@/domain/settings"; import { get as getSettings, save as saveSettings } from "@/repository/settings"; import { type ContactEmailsState, type ContactEmailsStore } from "./types"; export const useContactEmailsStore = create((set, get) => { let fetcher: Promise> | null = null; // 防止多次重复请求 let model: SettingsModel; // 记录当前设置的其他字段,保存回数据库时用 return { emails: [], loading: false, loadedAtOnce: false, fetchEmails: async (refresh = true) => { if (!refresh) { if (get().loadedAtOnce) { return get().emails; } } fetcher ??= getSettings(SETTINGS_NAMES.EMAILS); try { set({ loading: true }); model = await fetcher; set({ emails: model.content.emails?.filter((s) => !!s)?.sort() ?? [], loadedAtOnce: true }); } finally { fetcher = null; set({ loading: false }); } return get().emails; }, setEmails: async (emails) => { model ??= await getSettings(SETTINGS_NAMES.EMAILS); model = await saveSettings({ ...model, content: { ...model.content, emails: emails, }, }); set( produce((state: ContactEmailsState) => { state.emails = model.content.emails?.sort() ?? []; state.loadedAtOnce = true; }) ); }, addEmail: async (email) => { const emails = produce(get().emails, (draft) => { if (draft.includes(email)) return; draft.push(email); draft.sort(); return draft; }); get().setEmails(emails); }, removeEmail: async (email) => { const emails = produce(get().emails, (draft) => { draft = draft.filter((e) => e !== email); draft.sort(); return draft; }); get().setEmails(emails); }, }; }); ================================================ FILE: ui/src/stores/settings/contact/types.ts ================================================ export interface ContactEmailsState { emails: string[]; loading: boolean; loadedAtOnce: boolean; } export interface ContactEmailsActions { fetchEmails: (refresh?: boolean) => Promise; setEmails: (emails: string[]) => Promise; addEmail: (email: string) => Promise; removeEmail: (email: string) => Promise; } export interface ContactEmailsStore extends ContactEmailsState, ContactEmailsActions {} ================================================ FILE: ui/src/stores/settings/index.ts ================================================ export { useContactEmailsStore } from "./contact"; export { usePersistenceSettingsStore } from "./persistence"; export { useSSLProviderSettingsStore } from "./sslprovider"; export { useNotifyTemplatesStore, useScriptTemplatesStore } from "./template"; ================================================ FILE: ui/src/stores/settings/persistence/index.ts ================================================ import { produce } from "immer"; import { create } from "zustand"; import { type PersistenceSettingsContent, SETTINGS_NAMES, type SettingsModel } from "@/domain/settings"; import { get as getSettings, save as saveSettings } from "@/repository/settings"; import { type PersistenceSettingsState, type PersistenceSettingsStore } from "./types"; export const usePersistenceSettingsStore = create((set, get) => { let fetcher: Promise> | null = null; // 防止多次重复请求 let model: SettingsModel; // 记录当前设置的其他字段,保存回数据库时用 return { settings: {} as PersistenceSettingsContent, loading: false, loadedAtOnce: false, loadSettings: async (refresh = true) => { if (!refresh) { if (get().loadedAtOnce) { return; } } fetcher ??= getSettings(SETTINGS_NAMES.PERSISTENCE); try { set({ loading: true }); model = await fetcher; set({ settings: model.content, loadedAtOnce: true }); } finally { fetcher = null; set({ loading: false }); } }, saveSettings: async (settings) => { model ??= await getSettings(SETTINGS_NAMES.PERSISTENCE); model = await saveSettings({ ...model, content: settings, }); set( produce((state: PersistenceSettingsState) => { state.settings = model.content; state.loadedAtOnce = true; }) ); }, }; }); ================================================ FILE: ui/src/stores/settings/persistence/types.ts ================================================ import { type PersistenceSettingsContent } from "@/domain/settings"; export interface PersistenceSettingsState { settings: PersistenceSettingsContent; loading: boolean; loadedAtOnce: boolean; } export interface PersistenceSettingsActions { loadSettings: (refresh?: boolean) => Promise; saveSettings: (settings: PersistenceSettingsContent) => Promise; } export interface PersistenceSettingsStore extends PersistenceSettingsState, PersistenceSettingsActions {} ================================================ FILE: ui/src/stores/settings/sslprovider/index.ts ================================================ import { produce } from "immer"; import { create } from "zustand"; import { SETTINGS_NAMES, type SSLProviderSettingsContent, type SettingsModel } from "@/domain/settings"; import { get as getSettings, save as saveSettings } from "@/repository/settings"; import { type SSLProviderSettingsState, type SSLProviderSettingsStore } from "./types"; export const useSSLProviderSettingsStore = create((set, get) => { let fetcher: Promise> | null = null; // 防止多次重复请求 let model: SettingsModel; // 记录当前设置的其他字段,保存回数据库时用 return { settings: {} as SSLProviderSettingsContent, loading: false, loadedAtOnce: false, loadSettings: async (refresh = true) => { if (!refresh) { if (get().loadedAtOnce) { return; } } fetcher ??= getSettings(SETTINGS_NAMES.SSL_PROVIDER); try { set({ loading: true }); model = await fetcher; set({ settings: model.content, loadedAtOnce: true }); } finally { fetcher = null; set({ loading: false }); } }, saveSettings: async (settings) => { model ??= await getSettings(SETTINGS_NAMES.SSL_PROVIDER); model = await saveSettings({ ...model, content: settings, }); set( produce((state: SSLProviderSettingsState) => { state.settings = model.content; state.loadedAtOnce = true; }) ); }, }; }); ================================================ FILE: ui/src/stores/settings/sslprovider/types.ts ================================================ import { type SSLProviderSettingsContent } from "@/domain/settings"; export interface SSLProviderSettingsState { settings: SSLProviderSettingsContent; loading: boolean; loadedAtOnce: boolean; } export interface SSLProviderSettingsActions { loadSettings: (refresh?: boolean) => Promise; saveSettings: (settings: SSLProviderSettingsContent) => Promise; } export interface SSLProviderSettingsStore extends SSLProviderSettingsState, SSLProviderSettingsActions {} ================================================ FILE: ui/src/stores/settings/template/index.ts ================================================ import { produce } from "immer"; import { create } from "zustand"; import { type NotifyTemplateContent, SETTINGS_NAMES, type ScriptTemplateContent, type SettingsModel } from "@/domain/settings"; import { get as getSettings, save as saveSettings } from "@/repository/settings"; import { type NotifyTemplatesState, type NotifyTemplatesStore, type ScriptTemplatesState, type ScriptTemplatesStore } from "./types"; export const useNotifyTemplatesStore = create((set, get) => { let fetcher: Promise> | null = null; // 防止多次重复请求 let model: SettingsModel; // 记录当前设置的其他字段,保存回数据库时用 return { templates: [], loading: false, loadedAtOnce: false, fetchTemplates: async (refresh = true) => { if (!refresh) { if (get().loadedAtOnce) { return; } } fetcher ??= getSettings(SETTINGS_NAMES.NOTIFY_TEMPLATE); try { set({ loading: true }); model = await fetcher; set({ templates: model.content.templates ?? [], loadedAtOnce: true }); } finally { fetcher = null; set({ loading: false }); } }, setTemplates: async (templates) => { model ??= await getSettings(SETTINGS_NAMES.NOTIFY_TEMPLATE); model = await saveSettings({ ...model, content: { ...model.content, templates: templates, }, }); set( produce((state: NotifyTemplatesState) => { state.templates = model.content.templates ?? []; state.loadedAtOnce = true; }) ); }, addTemplate: async (template) => { const templates = produce(get().templates, (draft) => { const index = draft.findIndex((t) => t.name === template.name); if (index !== -1) { draft[index] = template; } else { draft.push(template); } return draft; }); get().setTemplates(templates); }, removeTemplateByIndex: async (index) => { const templates = produce(get().templates, (draft) => { draft = draft.filter((_, i) => i !== index); return draft; }); get().setTemplates(templates); }, removeTemplateByName: async (name) => { const templates = produce(get().templates, (draft) => { draft = draft.filter((e) => e.name !== name); return draft; }); get().setTemplates(templates); }, }; }); export const useScriptTemplatesStore = create((set, get) => { let fetcher: Promise> | null = null; // 防止多次重复请求 let model: SettingsModel; // 记录当前设置的其他字段,保存回数据库时用 return { templates: [], loading: false, loadedAtOnce: false, fetchTemplates: async (refresh = true) => { if (!refresh) { if (get().loadedAtOnce) { return; } } fetcher ??= getSettings(SETTINGS_NAMES.SCRIPT_TEMPLATE); try { set({ loading: true }); model = await fetcher; set({ templates: model.content.templates ?? [], loadedAtOnce: true }); } finally { fetcher = null; set({ loading: false }); } }, setTemplates: async (templates) => { model ??= await getSettings(SETTINGS_NAMES.SCRIPT_TEMPLATE); model = await saveSettings({ ...model, content: { ...model.content, templates: templates, }, }); set( produce((state: ScriptTemplatesState) => { state.templates = model.content.templates ?? []; state.loadedAtOnce = true; }) ); }, addTemplate: async (template) => { const templates = produce(get().templates, (draft) => { const index = draft.findIndex((t) => t.name === template.name); if (index !== -1) { draft[index] = template; } else { draft.push(template); } return draft; }); get().setTemplates(templates); }, removeTemplateByIndex: async (index) => { const templates = produce(get().templates, (draft) => { draft = draft.filter((_, i) => i !== index); return draft; }); get().setTemplates(templates); }, removeTemplateByName: async (name) => { const templates = produce(get().templates, (draft) => { draft = draft.filter((e) => e.name !== name); return draft; }); get().setTemplates(templates); }, }; }); ================================================ FILE: ui/src/stores/settings/template/types.ts ================================================ type NotifyTemplate = { name: string; subject: string; message: string; }; export interface NotifyTemplatesState { templates: NotifyTemplate[]; loading: boolean; loadedAtOnce: boolean; } export interface NotifyTemplatesActions { fetchTemplates: (refresh?: boolean) => Promise; setTemplates: (templates: NotifyTemplate[]) => Promise; addTemplate: (template: NotifyTemplate) => Promise; removeTemplateByIndex: (index: number) => Promise; removeTemplateByName: (name: string) => Promise; } export interface NotifyTemplatesStore extends NotifyTemplatesState, NotifyTemplatesActions {} type ScriptTemplate = { name: string; command: string; }; export interface ScriptTemplatesState { templates: ScriptTemplate[]; loading: boolean; loadedAtOnce: boolean; } export interface ScriptTemplatesActions { fetchTemplates: (refresh?: boolean) => Promise; setTemplates: (templates: ScriptTemplate[]) => Promise; addTemplate: (template: ScriptTemplate) => Promise; removeTemplateByIndex: (index: number) => Promise; removeTemplateByName: (name: string) => Promise; } export interface ScriptTemplatesStore extends ScriptTemplatesState, ScriptTemplatesActions {} ================================================ FILE: ui/src/stores/workflow/index.ts ================================================ import { produce } from "immer"; import { isEqual } from "radash"; import { create } from "zustand"; import { WORKFLOW_NODE_TYPES, type WorkflowModel, type WorkflowNodeConfigForStart } from "@/domain/workflow"; import { get as getWorkflow, save as saveWorkflow, subscribe as subscribeWorkflow } from "@/repository/workflow"; import { type WorkflowStore } from "./types"; export const useWorkflowStore = create((set, get) => { const ensureInitialized = () => { if (!get().initialized) throw "Workflow not initialized yet"; }; let unsubscriber: (() => void) | undefined; return { workflow: {} as WorkflowModel, initialized: false, init: async (id: string) => { const data = await getWorkflow(id); set({ workflow: data, initialized: true, }); unsubscriber ??= await subscribeWorkflow(id, (cb) => { if (cb.record.id !== get().workflow.id) return; set({ workflow: cb.record, }); }); }, destroy: () => { unsubscriber?.(); unsubscriber = void 0; set({ workflow: {} as WorkflowModel, initialized: false, }); }, setName: async (name) => { ensureInitialized(); const resp = await saveWorkflow({ id: get().workflow.id!, name: name || "", }); set((state) => { return { workflow: produce(state.workflow, (draft) => { draft.name = resp.name; }), }; }); }, setDescription: async (description) => { ensureInitialized(); const resp = await saveWorkflow({ id: get().workflow.id!, description: description || "", }); set((state) => { return { workflow: produce(state.workflow, (draft) => { draft.description = resp.description; }), }; }); }, setEnabled: async (enabled) => { ensureInitialized(); const resp = await saveWorkflow({ id: get().workflow.id!, enabled: enabled, }); set((state) => { return { workflow: produce(state.workflow, (draft) => { draft.enabled = resp.enabled; }), }; }); }, orchestrate: async (graph) => { ensureInitialized(); const resp = await saveWorkflow({ id: get().workflow.id!, graphDraft: graph, hasDraft: !isEqual(graph, get().workflow.graphContent), }); set((state) => { return { workflow: produce(state.workflow, (draft) => { draft.graphDraft = resp.graphDraft; draft.hasDraft = resp.hasDraft; }), }; }); }, publish: async () => { ensureInitialized(); const graph = get().workflow.graphDraft!; if (graph?.nodes?.[0]?.type !== WORKFLOW_NODE_TYPES.START) throw "Workflow nodes tree of draft in invalid"; const startConfig = graph.nodes[0].data.config as WorkflowNodeConfigForStart; const resp = await saveWorkflow({ id: get().workflow.id!, trigger: startConfig.trigger, triggerCron: startConfig.triggerCron, graphContent: graph, hasContent: true, hasDraft: false, }); set((state) => { return { workflow: produce(state.workflow, (draft) => { draft.trigger = resp.trigger; draft.triggerCron = resp.triggerCron; draft.graphContent = resp.graphContent; draft.hasContent = resp.hasContent; draft.hasDraft = resp.hasDraft; }), }; }); }, rollback: async () => { ensureInitialized(); const graph = get().workflow.graphContent!; if (graph?.nodes?.[0]?.type !== WORKFLOW_NODE_TYPES.START) throw "Workflow nodes tree of content in invalid"; const startConfig = graph.nodes[0].data.config as WorkflowNodeConfigForStart; const resp = await saveWorkflow({ id: get().workflow.id!, trigger: startConfig.trigger, triggerCron: startConfig.triggerCron, hasContent: true, graphDraft: graph, hasDraft: false, }); set((state) => { return { workflow: produce(state.workflow, (draft) => { draft.trigger = resp.trigger; draft.triggerCron = resp.triggerCron; draft.hasContent = resp.hasContent; draft.graphDraft = resp.graphDraft; draft.hasDraft = resp.hasDraft; }), }; }); }, }; }); ================================================ FILE: ui/src/stores/workflow/types.ts ================================================ import { type WorkflowGraph, type WorkflowModel } from "@/domain/workflow"; export interface WorkflowState { workflow: WorkflowModel; initialized: boolean; } export interface WorkflowActions { init(id: string): void; destroy(): void; setName: (name: Required["name"]) => void; setDescription: (description: Required["description"]) => void; setEnabled(enabled: Required["enabled"]): void; orchestrate(graph: WorkflowGraph): void; publish(): void; rollback(): void; } export interface WorkflowStore extends WorkflowState, WorkflowActions {} ================================================ FILE: ui/src/utils/browser.ts ================================================ export const isBrowserHappy = () => { try { if (typeof Promise.withResolvers !== "function") return false; if (typeof Promise.try !== "function") return false; if (typeof CSS.supports !== "function") return false; if (!CSS.supports("color", "oklch(0 0 0)")) return false; } catch (_) { return false; } return true; }; ================================================ FILE: ui/src/utils/cron.ts ================================================ import { CronExpressionParser } from "cron-parser"; export const validateCronExpression = (expr: string): boolean => { try { CronExpressionParser.parse(expr); // pocketbase 后端仅支持标准 crontab 形式的表达式 // 这里转译了来自 pocketbase 的 golang 代码来验证 const segments = expr.trim().split(" "); if (segments.length !== 5) return false; parseCronSegment(segments[0], 0, 59); parseCronSegment(segments[1], 0, 23); parseCronSegment(segments[2], 1, 31); parseCronSegment(segments[3], 1, 12); parseCronSegment(segments[4], 0, 6); return true; } catch { return false; } }; export const getNextCronExecutions = (expr: string, times = 1): Date[] => { if (!validateCronExpression(expr)) return []; const now = new Date(); const cron = CronExpressionParser.parse(expr, { currentDate: now }); return cron.take(times).map((date) => date.toDate()); }; // transpile from: // https://github.com/pocketbase/pocketbase/blob/5d964c1b1d020f425299b32df03ecf44e0a0502e/tools/cron/schedule.go#L141-L218 function parseCronSegment(segment: string, min: number, max: number): Set { const slots = new Set(); const list = segment.split(","); for (const p of list) { const stepParts = p.split("/"); let step: number; switch (stepParts.length) { case 1: { step = 1; } break; case 2: { const parsedStep = parseInt(stepParts[1], 10); if (isNaN(parsedStep) || parsedStep < 1 || parsedStep > max) { throw new Error(`Invalid segment step boundary - the step must be between 1 and the ${max}`); } step = parsedStep; } break; default: throw new Error("Invalid segment step format - must be in the format */n or 1-30/n"); } let rangeMin: number, rangeMax: number; if (stepParts[0] === "*") { rangeMin = min; rangeMax = max; } else { const rangeParts = stepParts[0].split("-"); switch (rangeParts.length) { case 1: { if (step !== 1) { throw new Error("Invalid segment step - step > 1 could be used only with the wildcard or range format"); } const parsed = parseInt(rangeParts[0], 10); if (isNaN(parsed) || parsed < min || parsed > max) { throw new Error("Invalid segment value - must be between the min and max of the segment"); } rangeMin = parsed; rangeMax = rangeMin; } break; case 2: { const parsedMin = parseInt(rangeParts[0], 10); if (isNaN(parsedMin) || parsedMin < min || parsedMin > max) { throw new Error(`Invalid segment range minimum - must be between ${min} and ${max}`); } rangeMin = parsedMin; const parsedMax = parseInt(rangeParts[1], 10); if (isNaN(parsedMax) || parsedMax < rangeMin || parsedMax > max) { throw new Error(`Invalid segment range maximum - must be between ${rangeMin} and ${max}`); } rangeMax = parsedMax; } break; default: throw new Error("Invalid segment range format - the range must have 1 or 2 parts"); } } for (let i = rangeMin; i <= rangeMax; i += step) { slots.add(i); } } return slots; } ================================================ FILE: ui/src/utils/css.ts ================================================ import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export const mergeCls = (...inputs: ClassValue[]) => { return twMerge(clsx(inputs)); }; ================================================ FILE: ui/src/utils/error.ts ================================================ import { ClientResponseError } from "pocketbase"; export const unwrapErrMsg = (error: unknown): string => { if (error instanceof ClientResponseError) { return Object.keys(error.response ?? {}).length ? unwrapErrMsg(error.response) : error.message; } else if (error instanceof Error) { return error.message; } else if (typeof error === "object" && error != null) { if ("message" in error) { return unwrapErrMsg(error.message); } else if ("msg" in error) { return unwrapErrMsg(error.msg); } } else if (typeof error === "string") { return error || "Unknown error"; } return "Unknown error"; }; ================================================ FILE: ui/src/utils/file.ts ================================================ export const readFileAsText = (file: File): Promise => { const { promise, resolve, reject } = Promise.withResolvers(); const reader = new FileReader(); reader.onload = () => { if (reader.result != null) { resolve(reader.result.toString()); } else { reject(new Error("Read file failed: result is null")); } }; reader.onerror = () => reject(reader.error); reader.readAsText(file, "utf-8"); return promise; }; ================================================ FILE: ui/src/utils/search.ts ================================================ export const matchSearchString = (keyword: string, candidate: string) => { keyword = String(keyword ?? "").toLowerCase(); candidate = String(candidate ?? "").toLowerCase(); if (keyword.length === 0) { return false; } if (candidate.includes(keyword)) { return true; } if (keyword.includes(" ")) { keyword = keyword.replaceAll(" ", ""); candidate = candidate.replaceAll(" ", ""); if (matchSearchString(keyword, candidate)) { return true; } } return false; }; export const matchSearchOption = (keyword: string, candidate: string | { label?: unknown } | { value?: unknown }) => { if (typeof candidate === "string") { return matchSearchString(keyword, candidate); } if ("label" in candidate && candidate.label != null) { if (matchSearchString(keyword, candidate.label as string)) { return true; } } if ("value" in candidate && candidate.value != null) { if (matchSearchString(keyword, candidate.value as string)) { return true; } } return false; }; ================================================ FILE: ui/src/utils/validator.ts ================================================ import { z } from "zod"; import { validateCronExpression } from "./cron"; export const isCron = (value: string) => { return validateCronExpression(value); }; export const isDomain = (value: string, { allowWildcard = false }: { allowWildcard?: boolean } = {}) => { const re = allowWildcard ? /^(?:\*\.)?(?!-)[A-Za-z0-9-]{1,}(? { return z.email().safeParse(value).success; }; export const isHostname = (value: string) => { return isDomain(value, { allowWildcard: false }) || isIPv4(value) || isIPv6(value); }; export const isIPv4 = (value: string) => { return z.ipv4().safeParse(value).success; }; export const isIPv6 = (value: string) => { return z.ipv6().safeParse(value).success; }; export const isJsonObject = (value: string) => { try { const obj = JSON.parse(value); return typeof obj === "object" && !Array.isArray(obj); } catch { return false; } }; export const isPortNumber = (value: string | number) => { return z.coerce.number().int().min(1).max(65535).safeParse(value).success; }; export const isUrlWithHttp = (value: string) => { return z.url().startsWith("http://").safeParse(value).success; }; export const isUrlWithHttps = (value: string) => { return z.url().startsWith("https://").safeParse(value).success; }; export const isUrlWithHttpOrHttps = (value: string) => { return isUrlWithHttp(value) || isUrlWithHttps(value); }; ================================================ FILE: ui/src/utils/x509.ts ================================================ import { ECPrivateKey } from "@peculiar/asn1-ecc"; import { PrivateKeyInfo } from "@peculiar/asn1-pkcs8"; import { RSAPrivateKey } from "@peculiar/asn1-rsa"; import { AsnParser } from "@peculiar/asn1-schema"; import { PemConverter, SubjectAlternativeNameExtension, X509Certificate } from "@peculiar/x509"; export const parseCertificate = (certPEM: string): X509Certificate => { if (!X509Certificate.isAsnEncoded(certPEM)) { throw new Error("Could not parse X.509 certificate. Maybe it is not in PEM format?"); } try { const cert = new X509Certificate(certPEM); if (cert == null) { throw new Error("Parse PEM certificate failed: result is null"); } return cert; } catch (err) { throw new Error("Could not parse X.509 certificate", { cause: err }); } }; export const getCertificateSubjectAltNames = (certificate: string | X509Certificate): string[] => { try { const certX509 = certificate instanceof X509Certificate ? certificate : parseCertificate(certificate); if (certX509 == null) return []; const sanExt = certX509.getExtension(SubjectAlternativeNameExtension); return sanExt?.names?.items?.map((san) => san.value) || []; } catch { return []; } }; export const parsePKCS1PrivateKey = (keyPEM: string): RSAPrivateKey => { try { const PEM_BLOCK_TYPE = "RSA PRIVATE KEY"; const pemBlock = PemConverter.decodeWithHeaders(keyPEM)[0]; if (!pemBlock || pemBlock.type !== PEM_BLOCK_TYPE) { throw new Error(`PEM block is not of type '${PEM_BLOCK_TYPE}'`); } const key = AsnParser.parse(pemBlock.rawData, RSAPrivateKey); if (key == null) { throw new Error("Read private key failed: result is null"); } return key; } catch (err) { throw new Error("Could not parse PKCS#1 RSA private key", { cause: err }); } }; export const parsePKCS8PrivateKey = (keyPEM: string): PrivateKeyInfo => { try { const PEM_BLOCK_TYPE = "PRIVATE KEY"; const pemBlock = PemConverter.decodeWithHeaders(keyPEM)[0]; if (!pemBlock || pemBlock.type !== PEM_BLOCK_TYPE) { throw new Error(`PEM block is not of type '${PEM_BLOCK_TYPE}'`); } const key = AsnParser.parse(pemBlock.rawData, PrivateKeyInfo); if (key == null) { throw new Error("Read private key failed: result is null"); } return key; } catch (err) { throw new Error("Could not parse PKCS#8 private key", { cause: err }); } }; export const parseECPrivateKey = (keyPEM: string): ECPrivateKey => { try { const PEM_BLOCK_TYPE = "EC PRIVATE KEY"; const pemBlock = PemConverter.decodeWithHeaders(keyPEM)[0]; if (!pemBlock || pemBlock.type !== PEM_BLOCK_TYPE) { throw new Error(`PEM block is not of type '${PEM_BLOCK_TYPE}'`); } const key = AsnParser.parse(pemBlock.rawData, ECPrivateKey); if (key == null) { throw new Error("Read private key failed: result is null"); } return key; } catch (err) { throw new Error("Could not parse EC private key", { cause: err }); } }; export const parsePrivateKey = (keyPEM: string): RSAPrivateKey | ECPrivateKey | PrivateKeyInfo => { try { return parsePKCS1PrivateKey(keyPEM); } catch { try { return parseECPrivateKey(keyPEM); } catch { return parsePKCS8PrivateKey(keyPEM); } } }; export const getPrivateKeyAlgorithm = (keyPEM: string): { algorithm?: "RSA" | "EC"; keySize?: number } => { try { const key = parsePrivateKey(keyPEM); if (key instanceof RSAPrivateKey) { return { algorithm: "RSA", keySize: (key.modulus.byteLength - 1) * 8 }; } if (key instanceof ECPrivateKey) { return { algorithm: "EC", keySize: key.privateKey.byteLength * 8 }; } if (key instanceof PrivateKeyInfo) { const OLD_PUBKEY_RSA = "1.2.840.113549.1.1.1"; const OLD_PUBKEY_ECDSA = "1.2.840.10045.2.1"; if (key.privateKeyAlgorithm.algorithm === OLD_PUBKEY_RSA) { const rsaKey = AsnParser.parse(key.privateKey, RSAPrivateKey); return { algorithm: "RSA", keySize: (rsaKey.modulus.byteLength - 1) * 8 }; } if (key.privateKeyAlgorithm.algorithm === OLD_PUBKEY_ECDSA) { const ecKey = AsnParser.parse(key.privateKey, ECPrivateKey); return { algorithm: "EC", keySize: ecKey.privateKey.byteLength * 8 }; } } return {}; } catch { return {}; } }; export const validatePEMCertificate = (certPEM: string): boolean => { try { const cert = parseCertificate(certPEM); return !!cert.getExtension(SubjectAlternativeNameExtension); } catch { return false; } }; export const validatePEMPrivateKey = (keyPEM: string): boolean => { try { parsePrivateKey(keyPEM); return true; } catch { return false; } }; ================================================ FILE: ui/tsconfig.app.json ================================================ { "compilerOptions": { "composite": true, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "ES2020", "useDefineForClassFields": true, "lib": [ "DOM", "DOM.Iterable", "ESNext", ], "module": "ESNext", "skipLibCheck": true, "baseUrl": ".", "paths": { "@/*": [ "./src/*" ] }, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": [ "src", "types" ] } ================================================ FILE: ui/tsconfig.json ================================================ { "files": [], "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" } ], "compilerOptions": { "baseUrl": ".", "paths": { "@/*": [ "src/*" ] } } } ================================================ FILE: ui/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "strict": true, "noEmit": true }, "include": [ "vite.config.ts" ] } ================================================ FILE: ui/types/global.d.ts ================================================ import { type BaseModel as PbBaseModel } from "pocketbase"; declare global { declare type ISO8601String = string; declare interface BaseModel extends PbBaseModel { created: ISO8601String; updated: ISO8601String; deleted?: ISO8601String; } declare type MaybeModelRecord = T | Omit; declare type MaybeModelRecordWithId = T | Pick; declare interface BaseResponse { code: number; msg: string; data: T; } } export {}; ================================================ FILE: ui/types/global.utility.d.ts ================================================ declare global { type Nullish = { [P in keyof T]?: T[P] | null | undefined; }; type ArrayElement = T extends (infer U)[] ? U : never; } export {}; ================================================ FILE: ui/types/shims-antd.d.ts ================================================ declare module "antd/locale/zh_CN" { import zhCN from "antd/locale/zh_CN"; export default zhCN; } ================================================ FILE: ui/types/vite-env.d.ts ================================================ /// declare const __APP_VERSION__: string; ================================================ FILE: ui/vite.config.ts ================================================ import path from "node:path"; import tailwindcssPlugin from "@tailwindcss/vite"; import legacyPlugin from "@vitejs/plugin-legacy"; import reactPlugin from "@vitejs/plugin-react"; import fs from "fs-extra"; import { type Plugin, defineConfig } from "vite"; const preserveFilesPlugin = (filesToPreserve: string[]): Plugin => { return { name: "preserve-files", apply: "build", buildStart() { // 在构建开始时将要保留的文件或目录移动到临时位置 filesToPreserve.forEach((file) => { const srcPath = path.resolve(__dirname, file); const tempPath = path.resolve(__dirname, `node_modules/.tmp/build/${file}`); if (fs.existsSync(srcPath)) { fs.moveSync(srcPath, tempPath, { overwrite: true }); } }); }, closeBundle() { // 在构建完成后将临时位置的文件或目录移回原来的位置 filesToPreserve.forEach((file) => { const srcPath = path.resolve(__dirname, file); const tempPath = path.resolve(__dirname, `node_modules/.tmp/build/${file}`); if (fs.existsSync(tempPath)) { fs.moveSync(tempPath, srcPath, { overwrite: true }); } }); }, }; }; export default defineConfig(() => { let appVersion = undefined; try { const content = fs.readFileSync(path.resolve(__dirname, "../internal/app/app.go"), "utf-8"); const matches = content.match(/AppVersion\s+=\s+"(.+?)"/); if (matches) { appVersion = matches[1]; console.info("[certimate] AppVersion is " + appVersion); } else { throw new Error("AppVersion not found in '/internal/app/app.go'"); } } catch (err) { throw new Error("Could not read app version: " + (err as Error).message); } return { define: { __APP_VERSION__: JSON.stringify(appVersion), }, build: { rollupOptions: { output: { manualChunks(id) { if (id.includes("/src/i18n/")) { return "locales"; } }, }, }, }, plugins: [ reactPlugin({}), legacyPlugin({ targets: ["defaults", "not IE 11"], modernTargets: "chrome>=111, firefox>=113, safari>=15.4", polyfills: true, modernPolyfills: true, renderLegacyChunks: false, renderModernChunks: true, }), tailwindcssPlugin(), preserveFilesPlugin(["dist/.gitkeep"]), ], resolve: { alias: { "@": path.resolve(__dirname, "./src"), }, }, server: { proxy: { "/api": "http://127.0.0.1:8090", }, }, }; });