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
[](https://github.com/certimate-go/certimate)
[](https://github.com/certimate-go/certimate)
[](https://hub.docker.com/r/certimate/certimate)
[](https://github.com/certimate-go/certimate/releases)
[](https://mit-license.org/)
[](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
[](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
[](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.
[](https://starchart.cc/certimate-go/certimate)
================================================
FILE: README_zh.md
================================================
🔒 Certimate
[](https://github.com/certimate-go/certimate)
[](https://github.com/certimate-go/certimate)
[](https://hub.docker.com/r/certimate/certimate)
[](https://github.com/certimate-go/certimate/releases)
[](https://mit-license.org/)
[](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)
## 🖥️ 运行界面
[](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 做出的贡献:
[](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 星标关注,即可第一时间获取新版本发布通知。
[](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"));
}}
>
{children}
);
};
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}
{cancelText ?? t("common.button.cancel")}
{okText ?? t("common.button.ok")}
}
forceRender
open={open}
title={
{title}
{mergedDrawerProps.closeIcon !== false && (
}
size="small"
type="text"
onClick={handleCancelClick}
/>
)}
}
>
>
);
};
export default DrawerForm;
================================================
FILE: ui/src/components/Empty.tsx
================================================
import { useTranslation } from "react-i18next";
import { Typography, theme } from "antd";
import Show from "./Show";
export interface EmptyProps {
className?: string;
style?: React.CSSProperties;
title?: React.ReactNode;
description?: React.ReactNode;
extra?: React.ReactNode;
icon?: React.ReactNode;
}
const Empty = (props: EmptyProps) => {
const { t } = useTranslation();
const { className, style, title = t("common.text.nodata"), description, extra, icon } = props;
const { token: themeToken } = theme.useToken();
const isPrimitive = (node: React.ReactNode): node is string | number | boolean | null => {
return typeof node === "string" || typeof node === "number" || typeof node === "boolean" || node == null;
};
return (
{title}
{isPrimitive(description) ? (
{description}
) : (
{description}
)}
{extra}
);
};
export default Empty;
================================================
FILE: ui/src/components/FileTextInput.tsx
================================================
import { type ChangeEvent, useContext, useRef } from "react";
import { useTranslation } from "react-i18next";
import { IconFileImport } from "@tabler/icons-react";
import { Button, type ButtonProps, Input } from "antd";
import DisabledContext from "antd/es/config-provider/DisabledContext";
import { type TextAreaProps } from "antd/es/input/TextArea";
import { readFileAsText } from "@/utils/file";
export interface FileTextInputProps extends Omit {
accept?: string;
uploadButtonProps?: Omit;
uploadText?: string;
onChange?: (value: string) => void;
}
const FileTextInput = ({ className, style, accept, disabled, readOnly, uploadText, uploadButtonProps, onChange, ...props }: FileTextInputProps) => {
const { t } = useTranslation();
const injectedDisabled = useContext(DisabledContext);
const mergedDisabled = disabled ?? injectedDisabled;
const fileInputRef = useRef(null);
const handleButtonClick = () => {
if (fileInputRef.current) {
fileInputRef.current.click();
}
};
const handleFileChange = async (e: ChangeEvent) => {
const { files } = e.target as HTMLInputElement;
if (files?.length) {
const value = await readFileAsText(files[0]);
onChange?.(value);
}
};
return (
);
};
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}
>
);
};
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 ? (
{t("common.button.add")}
) : (
{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 } color="default" disabled={disabled || !allowUp} type="text" onClick={onEntryUp} />;
}, [allowUp, disabled, showSortButton, onEntryUp]);
const downBtn = useMemo(() => {
if (!showSortButton) return null;
return } color="default" disabled={disabled || !allowDown} type="text" onClick={onEntryDown} />;
}, [allowDown, disabled, showSortButton, onEntryDown]);
const removeBtn = useMemo(() => {
return } color="default" disabled={disabled || !allowRemove} type="text" onClick={onEntryRemove} />;
}, [allowRemove, disabled, onEntryRemove]);
const addBtn = useMemo(() => {
return } color="default" disabled={disabled || !allowAdd} type="text" onClick={onEntryAdd} />;
}, [allowAdd, disabled, onEntryAdd]);
const handleInputChange = (e: ChangeEvent) => {
setValue(e.target.value);
};
useImperativeHandle(ref, () => ({
focus: (options) => {
inputRef.current?.focus(options);
},
blur: () => {
inputRef.current?.blur();
},
select: () => {
inputRef.current?.select();
},
}));
return (
);
}
);
export default MultipleInput;
================================================
FILE: ui/src/components/MultipleSplitValueInput.tsx
================================================
import { type ChangeEvent, useEffect } from "react";
import { IconList } from "@tabler/icons-react";
import { useControllableValue } from "ahooks";
import { Button, Form, Input, type InputProps, Space } from "antd";
import { nanoid } from "nanoid/non-secure";
import { useAntdForm } from "@/hooks";
import ModalForm from "./ModalForm";
import MultipleInput from "./MultipleInput";
type SplitOptions = {
removeEmpty?: boolean;
trimSpace?: boolean;
};
export interface MultipleSplitValueInputProps extends Omit {
defaultValue?: string;
maxCount?: number;
minCount?: number;
modalTitle?: React.ReactNode;
modalWidth?: number | string;
placeholderInModal?: string;
showSortButton?: boolean;
separator?: string;
splitOptions?: SplitOptions;
value?: string[];
onChange?: (value: string) => void;
}
const DEFAULT_SEPARATOR = ";";
const MultipleSplitValueInput = ({
className,
style,
size,
separator: delimiter = DEFAULT_SEPARATOR,
disabled,
maxCount,
minCount,
modalTitle,
modalWidth = "480px",
placeholder,
placeholderInModal,
showSortButton = true,
splitOptions = {},
onClear,
...props
}: MultipleSplitValueInputProps) => {
const [value, setValue] = useControllableValue(props, {
valuePropName: "value",
defaultValuePropName: "defaultValue",
trigger: "onChange",
});
const { form: formInst, formProps } = useAntdForm({
name: "componentMultipleSplitValueInput_" + nanoid(),
initialValues: { value: value?.split(delimiter) },
onSubmit: (values) => {
const temp = (values.value ?? []) as string[];
if (splitOptions.trimSpace) {
temp.map((e) => e.trim());
}
if (splitOptions.removeEmpty) {
temp.filter((e) => !!e);
}
setValue(temp.join(delimiter));
},
});
useEffect(() => {
formInst.setFieldValue("value", value?.split(delimiter));
}, [delimiter, value]);
const handleChange = (e: ChangeEvent) => {
setValue(e.target.value);
};
const handleClear = () => {
setValue("");
onClear?.();
};
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" ? (
{t("access.action.test_notify.button")}
) : (
{/* TODO: 测试连接 */}
)}
{t("common.button.cancel")}
{mode === "modify" ? t("common.button.save") : t("common.button.submit")}
} type="primary" />
) : (
false
)
}
loading={loading}
maskClosable={!formPending}
open={open}
size="large"
title={
{mode === "modify" && !!data?.id ? t("access.action.modify.modal.title") + ` #${data.id}` : t(`access.action.${mode}.modal.title`)}
}
size="small"
type="text"
onClick={handleCancelClick}
/>
}
onClose={handleCancelClick}
>
>
);
};
const useDrawer = () => {
type DataType = AccessEditDrawerProps["data"];
const [data, setData, getData] = useGetState();
const [loading, setLoading] = useState();
const [open, setOpen] = useState(false);
const onOpenChange = useCallback((open: boolean) => {
setOpen(open);
}, []);
return {
drawerProps: {
afterClose: () => {
startTransition(() => {
if (!open) {
setData(void 0);
setLoading(void 0);
}
});
},
data,
loading,
open,
onOpenChange,
},
open: ({ data, loading }: { data: NonNullable; loading?: boolean }) => {
setData(data);
setLoading(loading);
setOpen(true);
return {
safeUpdate: ({ data, loading }: { data?: NonNullable; loading?: boolean }) => {
if (data != null) {
if (data.id !== getData()?.id) return; // 确保数据不脏读
setData(data);
}
if (loading != null) {
setLoading(loading);
}
},
};
},
close: () => {
setOpen(false);
},
};
};
const _default = Object.assign(AccessEditDrawer, {
useDrawer,
});
export default _default;
================================================
FILE: ui/src/components/access/AccessForm.tsx
================================================
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import AccessProviderSelect from "@/components/provider/AccessProviderSelect";
import { type AccessModel } from "@/domain/access";
import { ACCESS_PROVIDERS, ACCESS_USAGES } from "@/domain/provider";
import { useAntdForm } from "@/hooks";
import { FormNestedFieldsContextProvider } from "./forms/_context";
import { useProviderFilterByUsage } from "./forms/_hooks";
import AccessConfigFieldsProvider from "./forms/AccessConfigFieldsProvider";
export type AccessFormModes = "create" | "modify";
export type AccessFormUsages = "dns" | "hosting" | "dns-hosting" | "ca" | "notification";
export interface AccessFormProps {
className?: string;
style?: React.CSSProperties;
disabled?: boolean;
initialValues?: Nullish>;
form: FormInstance;
mode: AccessFormModes;
usage?: AccessFormUsages;
onFormValuesChange?: (changedValues: Nullish>, values: Nullish>) => void;
}
const AccessForm = ({ className, style, disabled, initialValues, mode, usage, onFormValuesChange, ...props }: AccessFormProps) => {
const { t } = useTranslation();
const providerFilter = useProviderFilterByUsage(usage);
const formSchema = z.object({
name: z
.string(t("access.form.name.placeholder"))
.min(1, t("access.form.name.placeholder"))
.max(64, t("common.errmsg.string_max", { max: 64 })),
provider: z.enum(ACCESS_PROVIDERS, t("access.form.provider.placeholder")),
config: z.any(),
reserve: z.string().nullish(),
});
const formRule = createSchemaFieldRule(formSchema);
const { form: formInst, formProps } = useAntdForm>({
form: props.form,
name: "accessForm",
initialValues: initialValues,
});
const fieldProvider = Form.useWatch("provider", { form: formInst, preserve: true });
const renderNestedFieldProviderComponent = AccessConfigFieldsProvider.useComponent(fieldProvider, {
initProps: (provider) => {
let props: object = { disabled: disabled };
switch (provider) {
case ACCESS_PROVIDERS.WEBHOOK:
{
props = {
...props,
usage: usage === "notification" ? "notification" : usage === "hosting" || usage === "dns-hosting" ? "deployment" : "none",
};
}
break;
}
return props;
},
deps: [disabled, usage],
});
return (
: null}
rules={[formRule]}
>
{renderNestedFieldProviderComponent && <>{renderNestedFieldProviderComponent}>}
);
};
const _default = Object.assign(AccessForm, {
useProviderFilterByUsage,
});
export default _default;
================================================
FILE: ui/src/components/access/AccessSelect.tsx
================================================
import { useEffect, useState } from "react";
import { useMount } from "ahooks";
import { Avatar, Select, type SelectProps, Typography, theme } from "antd";
import { type AccessModel } from "@/domain/access";
import { accessProvidersMap } from "@/domain/provider";
import { useZustandShallowSelector } from "@/hooks";
import { useAccessesStore } from "@/stores/access";
import { matchSearchOption } from "@/utils/search";
export interface AccessTypeSelectProps extends Omit {
onFilter?: (value: string, option: AccessModel) => boolean;
}
const AccessSelect = ({ onFilter, ...props }: AccessTypeSelectProps) => {
const { token: themeToken } = theme.useToken();
const { accesses, loadedAtOnce, fetchAccesses } = useAccessesStore(useZustandShallowSelector(["accesses", "loadedAtOnce", "fetchAccesses"]));
useMount(() => {
fetchAccesses(false);
});
const [options, setOptions] = useState>([]);
useEffect(() => {
const filteredItems = onFilter != null ? accesses.filter((item) => onFilter(item.id, item)) : accesses;
setOptions(
filteredItems.map((item) => ({
key: item.id,
value: item.id,
label: item.name,
data: item,
}))
);
}, [accesses, onFilter]);
const renderOption = (key: string) => {
const access = accesses.find((e) => e.id === key);
if (!access) {
return (
);
}
const provider = accessProvidersMap.get(access.provider);
return (
);
};
return (
matchSearchOption(inputValue, option!),
optionFilterProp: "label",
}}
labelRender={({ value }) => {
if (value != null) {
return renderOption(value as string);
}
return {props.placeholder} ;
}}
loading={!loadedAtOnce}
options={options}
optionLabelProp={void 0}
optionRender={(option) => renderOption(option.data.value)}
/>
);
};
export default AccessSelect;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProvider.tsx
================================================
import { useEffect, useState } from "react";
import { ACCESS_PROVIDERS, type AccessProviderType } from "@/domain/provider";
import AccessConfigFieldsProvider1Panel from "./AccessConfigFieldsProvider1Panel";
import AccessConfigFieldsProvider35cn from "./AccessConfigFieldsProvider35cn";
import AccessConfigFieldsProvider51DNScom from "./AccessConfigFieldsProvider51DNScom";
import AccessConfigFieldsProviderACMECA from "./AccessConfigFieldsProviderACMECA";
import AccessConfigFieldsProviderACMEDNS from "./AccessConfigFieldsProviderACMEDNS";
import AccessConfigFieldsProviderACMEHttpReq from "./AccessConfigFieldsProviderACMEHttpReq";
import AccessConfigFieldsProviderActalisSSL from "./AccessConfigFieldsProviderActalisSSL";
import AccessConfigFieldsProviderAkamai from "./AccessConfigFieldsProviderAkamai";
import AccessConfigFieldsProviderAliyun from "./AccessConfigFieldsProviderAliyun";
import AccessConfigFieldsProviderAPISIX from "./AccessConfigFieldsProviderAPISIX";
import AccessConfigFieldsProviderArvanCloud from "./AccessConfigFieldsProviderArvanCloud";
import AccessConfigFieldsProviderAWS from "./AccessConfigFieldsProviderAWS";
import AccessConfigFieldsProviderAzure from "./AccessConfigFieldsProviderAzure";
import AccessConfigFieldsProviderBaiduCloud from "./AccessConfigFieldsProviderBaiduCloud";
import AccessConfigFieldsProviderBaishan from "./AccessConfigFieldsProviderBaishan";
import AccessConfigFieldsProviderBaotaPanel from "./AccessConfigFieldsProviderBaotaPanel";
import AccessConfigFieldsProviderBaotaPanelGo from "./AccessConfigFieldsProviderBaotaPanelGo";
import AccessConfigFieldsProviderBaotaWAF from "./AccessConfigFieldsProviderBaotaWAF";
import AccessConfigFieldsProviderBookMyName from "./AccessConfigFieldsProviderBookMyName";
import AccessConfigFieldsProviderBunny from "./AccessConfigFieldsProviderBunny";
import AccessConfigFieldsProviderBytePlus from "./AccessConfigFieldsProviderBytePlus";
import AccessConfigFieldsProviderCacheFly from "./AccessConfigFieldsProviderCacheFly";
import AccessConfigFieldsProviderCdnfly from "./AccessConfigFieldsProviderCdnfly";
import AccessConfigFieldsProviderCloudflare from "./AccessConfigFieldsProviderCloudflare";
import AccessConfigFieldsProviderClouDNS from "./AccessConfigFieldsProviderClouDNS";
import AccessConfigFieldsProviderCMCCCloud from "./AccessConfigFieldsProviderCMCCCloud";
import AccessConfigFieldsProviderConstellix from "./AccessConfigFieldsProviderConstellix";
import AccessConfigFieldsProviderCPanel from "./AccessConfigFieldsProviderCPanel";
import AccessConfigFieldsProviderCTCCCloud from "./AccessConfigFieldsProviderCTCCCloud";
import AccessConfigFieldsProviderDeSEC from "./AccessConfigFieldsProviderDeSEC";
import AccessConfigFieldsProviderDigiCert from "./AccessConfigFieldsProviderDigiCert";
import AccessConfigFieldsProviderDigitalOcean from "./AccessConfigFieldsProviderDigitalOcean";
import AccessConfigFieldsProviderDingTalkBot from "./AccessConfigFieldsProviderDingTalkBot";
import AccessConfigFieldsProviderDiscordBot from "./AccessConfigFieldsProviderDiscordBot";
import AccessConfigFieldsProviderDNSExit from "./AccessConfigFieldsProviderDNSExit";
import AccessConfigFieldsProviderDNSLA from "./AccessConfigFieldsProviderDNSLA";
import AccessConfigFieldsProviderDNSMadeEasy from "./AccessConfigFieldsProviderDNSMadeEasy";
import AccessConfigFieldsProviderDogeCloud from "./AccessConfigFieldsProviderDogeCloud";
import AccessConfigFieldsProviderDokploy from "./AccessConfigFieldsProviderDokploy";
import AccessConfigFieldsProviderDuckDNS from "./AccessConfigFieldsProviderDuckDNS";
import AccessConfigFieldsProviderDynu from "./AccessConfigFieldsProviderDynu";
import AccessConfigFieldsProviderDynv6 from "./AccessConfigFieldsProviderDynv6";
import AccessConfigFieldsProviderEmail from "./AccessConfigFieldsProviderEmail";
import AccessConfigFieldsProviderFlexCDN from "./AccessConfigFieldsProviderFlexCDN";
import AccessConfigFieldsProviderFlyIO from "./AccessConfigFieldsProviderFlyIO";
import AccessConfigFieldsProviderGandinet from "./AccessConfigFieldsProviderGandinet";
import AccessConfigFieldsProviderGcore from "./AccessConfigFieldsProviderGcore";
import AccessConfigFieldsProviderGlobalSignAtlas from "./AccessConfigFieldsProviderGlobalSignAtlas";
import AccessConfigFieldsProviderGname from "./AccessConfigFieldsProviderGname";
import AccessConfigFieldsProviderGoDaddy from "./AccessConfigFieldsProviderGoDaddy";
import AccessConfigFieldsProviderGoEdge from "./AccessConfigFieldsProviderGoEdge";
import AccessConfigFieldsProviderGoogleTrustServices from "./AccessConfigFieldsProviderGoogleTrustServices";
import AccessConfigFieldsProviderHetzner from "./AccessConfigFieldsProviderHetzner";
import AccessConfigFieldsProviderHostingde from "./AccessConfigFieldsProviderHostingde";
import AccessConfigFieldsProviderHostinger from "./AccessConfigFieldsProviderHostinger";
import AccessConfigFieldsProviderHuaweiCloud from "./AccessConfigFieldsProviderHuaweiCloud";
import AccessConfigFieldsProviderInfomaniak from "./AccessConfigFieldsProviderInfomaniak";
import AccessConfigFieldsProviderIONOS from "./AccessConfigFieldsProviderIONOS";
import AccessConfigFieldsProviderJDCloud from "./AccessConfigFieldsProviderJDCloud";
import AccessConfigFieldsProviderKong from "./AccessConfigFieldsProviderKong";
import AccessConfigFieldsProviderKsyun from "./AccessConfigFieldsProviderKsyun";
import AccessConfigFieldsProviderKubernetes from "./AccessConfigFieldsProviderKubernetes";
import AccessConfigFieldsProviderLarkBot from "./AccessConfigFieldsProviderLarkBot";
import AccessConfigFieldsProviderLeCDN from "./AccessConfigFieldsProviderLeCDN";
import AccessConfigFieldsProviderLinode from "./AccessConfigFieldsProviderLinode";
import AccessConfigFieldsProviderLiteSSL from "./AccessConfigFieldsProviderLiteSSL";
import AccessConfigFieldsProviderMattermost from "./AccessConfigFieldsProviderMattermost";
import AccessConfigFieldsProviderMohua from "./AccessConfigFieldsProviderMohua";
import AccessConfigFieldsProviderNamecheap from "./AccessConfigFieldsProviderNamecheap";
import AccessConfigFieldsProviderNameDotCom from "./AccessConfigFieldsProviderNameDotCom";
import AccessConfigFieldsProviderNameSilo from "./AccessConfigFieldsProviderNameSilo";
import AccessConfigFieldsProviderNetcup from "./AccessConfigFieldsProviderNetcup";
import AccessConfigFieldsProviderNetlify from "./AccessConfigFieldsProviderNetlify";
import AccessConfigFieldsProviderNginxProxyManager from "./AccessConfigFieldsProviderNginxProxyManager";
import AccessConfigFieldsProviderNS1 from "./AccessConfigFieldsProviderNS1";
import AccessConfigFieldsProviderOVHcloud from "./AccessConfigFieldsProviderOVHcloud";
import AccessConfigFieldsProviderPorkbun from "./AccessConfigFieldsProviderPorkbun";
import AccessConfigFieldsProviderPowerDNS from "./AccessConfigFieldsProviderPowerDNS";
import AccessConfigFieldsProviderProxmoxVE from "./AccessConfigFieldsProviderProxmoxVE";
import AccessConfigFieldsProviderQingCloud from "./AccessConfigFieldsProviderQingCloud";
import AccessConfigFieldsProviderQiniu from "./AccessConfigFieldsProviderQiniu";
import AccessConfigFieldsProviderRainYun from "./AccessConfigFieldsProviderRainYun";
import AccessConfigFieldsProviderRatPanel from "./AccessConfigFieldsProviderRatPanel";
import AccessConfigFieldsProviderRFC2136 from "./AccessConfigFieldsProviderRFC2136";
import AccessConfigFieldsProviderS3 from "./AccessConfigFieldsProviderS3";
import AccessConfigFieldsProviderSafeLine from "./AccessConfigFieldsProviderSafeLine";
import AccessConfigFieldsProviderSectigo from "./AccessConfigFieldsProviderSectigo";
import AccessConfigFieldsProviderSlackBot from "./AccessConfigFieldsProviderSlackBot";
import AccessConfigFieldsProviderSpaceship from "./AccessConfigFieldsProviderSpaceship";
import AccessConfigFieldsProviderSSH from "./AccessConfigFieldsProviderSSH";
import AccessConfigFieldsProviderSSLCom from "./AccessConfigFieldsProviderSSLCom";
import AccessConfigFieldsProviderSynologyDSM from "./AccessConfigFieldsProviderSynologyDSM";
import AccessConfigFieldsProviderTechnitiumDNS from "./AccessConfigFieldsProviderTechnitiumDNS";
import AccessConfigFieldsProviderTelegramBot from "./AccessConfigFieldsProviderTelegramBot";
import AccessConfigFieldsProviderTencentCloud from "./AccessConfigFieldsProviderTencentCloud";
import AccessConfigFieldsProviderTodayNIC from "./AccessConfigFieldsProviderTodayNIC";
import AccessConfigFieldsProviderUCloud from "./AccessConfigFieldsProviderUCloud";
import AccessConfigFieldsProviderUniCloud from "./AccessConfigFieldsProviderUniCloud";
import AccessConfigFieldsProviderUpyun from "./AccessConfigFieldsProviderUpyun";
import AccessConfigFieldsProviderVercel from "./AccessConfigFieldsProviderVercel";
import AccessConfigFieldsProviderVolcEngine from "./AccessConfigFieldsProviderVolcEngine";
import AccessConfigFieldsProviderVultr from "./AccessConfigFieldsProviderVultr";
import AccessConfigFieldsProviderWangsu from "./AccessConfigFieldsProviderWangsu";
import AccessConfigFieldsProviderWebhook from "./AccessConfigFieldsProviderWebhook";
import AccessConfigFieldsProviderWeComBot from "./AccessConfigFieldsProviderWeComBot";
import AccessConfigFieldsProviderWestcn from "./AccessConfigFieldsProviderWestcn";
import AccessConfigFieldsProviderXinnet from "./AccessConfigFieldsProviderXinnet";
import AccessConfigFieldsProviderZeroSSL from "./AccessConfigFieldsProviderZeroSSL";
const providerComponentMap: Partial>> = {
/*
注意:如果追加新的子组件,请保持以 ASCII 排序。
NOTICE: If you add new child component, please keep ASCII order.
*/
[ACCESS_PROVIDERS["1PANEL"]]: AccessConfigFieldsProvider1Panel,
[ACCESS_PROVIDERS["35CN"]]: AccessConfigFieldsProvider35cn,
[ACCESS_PROVIDERS["51DNSCOM"]]: AccessConfigFieldsProvider51DNScom,
[ACCESS_PROVIDERS.ACMECA]: AccessConfigFieldsProviderACMECA,
[ACCESS_PROVIDERS.ACMEDNS]: AccessConfigFieldsProviderACMEDNS,
[ACCESS_PROVIDERS.ACMEHTTPREQ]: AccessConfigFieldsProviderACMEHttpReq,
[ACCESS_PROVIDERS.ACTALISSSL]: AccessConfigFieldsProviderActalisSSL,
[ACCESS_PROVIDERS.AKAMAI]: AccessConfigFieldsProviderAkamai,
[ACCESS_PROVIDERS.ALIYUN]: AccessConfigFieldsProviderAliyun,
[ACCESS_PROVIDERS.APISIX]: AccessConfigFieldsProviderAPISIX,
[ACCESS_PROVIDERS.ARVANCLOUD]: AccessConfigFieldsProviderArvanCloud,
[ACCESS_PROVIDERS.AWS]: AccessConfigFieldsProviderAWS,
[ACCESS_PROVIDERS.AZURE]: AccessConfigFieldsProviderAzure,
[ACCESS_PROVIDERS.BAIDUCLOUD]: AccessConfigFieldsProviderBaiduCloud,
[ACCESS_PROVIDERS.BAISHAN]: AccessConfigFieldsProviderBaishan,
[ACCESS_PROVIDERS.BAOTAPANEL]: AccessConfigFieldsProviderBaotaPanel,
[ACCESS_PROVIDERS.BAOTAPANELGO]: AccessConfigFieldsProviderBaotaPanelGo,
[ACCESS_PROVIDERS.BAOTAWAF]: AccessConfigFieldsProviderBaotaWAF,
[ACCESS_PROVIDERS.BOOKMYNAME]: AccessConfigFieldsProviderBookMyName,
[ACCESS_PROVIDERS.BUNNY]: AccessConfigFieldsProviderBunny,
[ACCESS_PROVIDERS.BYTEPLUS]: AccessConfigFieldsProviderBytePlus,
[ACCESS_PROVIDERS.CACHEFLY]: AccessConfigFieldsProviderCacheFly,
[ACCESS_PROVIDERS.CDNFLY]: AccessConfigFieldsProviderCdnfly,
[ACCESS_PROVIDERS.CLOUDFLARE]: AccessConfigFieldsProviderCloudflare,
[ACCESS_PROVIDERS.CLOUDNS]: AccessConfigFieldsProviderClouDNS,
[ACCESS_PROVIDERS.CMCCCLOUD]: AccessConfigFieldsProviderCMCCCloud,
[ACCESS_PROVIDERS.CONSTELLIX]: AccessConfigFieldsProviderConstellix,
[ACCESS_PROVIDERS.CPANEL]: AccessConfigFieldsProviderCPanel,
[ACCESS_PROVIDERS.CTCCCLOUD]: AccessConfigFieldsProviderCTCCCloud,
[ACCESS_PROVIDERS.DESEC]: AccessConfigFieldsProviderDeSEC,
[ACCESS_PROVIDERS.DIGICERT]: AccessConfigFieldsProviderDigiCert,
[ACCESS_PROVIDERS.DIGITALOCEAN]: AccessConfigFieldsProviderDigitalOcean,
[ACCESS_PROVIDERS.DINGTALKBOT]: AccessConfigFieldsProviderDingTalkBot,
[ACCESS_PROVIDERS.DISCORDBOT]: AccessConfigFieldsProviderDiscordBot,
[ACCESS_PROVIDERS.DNSEXIT]: AccessConfigFieldsProviderDNSExit,
[ACCESS_PROVIDERS.DNSLA]: AccessConfigFieldsProviderDNSLA,
[ACCESS_PROVIDERS.DNSMADEEASY]: AccessConfigFieldsProviderDNSMadeEasy,
[ACCESS_PROVIDERS.DOKPLOY]: AccessConfigFieldsProviderDokploy,
[ACCESS_PROVIDERS.DOGECLOUD]: AccessConfigFieldsProviderDogeCloud,
[ACCESS_PROVIDERS.DUCKDNS]: AccessConfigFieldsProviderDuckDNS,
[ACCESS_PROVIDERS.DYNU]: AccessConfigFieldsProviderDynu,
[ACCESS_PROVIDERS.DYNV6]: AccessConfigFieldsProviderDynv6,
[ACCESS_PROVIDERS.EMAIL]: AccessConfigFieldsProviderEmail,
[ACCESS_PROVIDERS.FLEXCDN]: AccessConfigFieldsProviderFlexCDN,
[ACCESS_PROVIDERS.FLYIO]: AccessConfigFieldsProviderFlyIO,
[ACCESS_PROVIDERS.GANDINET]: AccessConfigFieldsProviderGandinet,
[ACCESS_PROVIDERS.GCORE]: AccessConfigFieldsProviderGcore,
[ACCESS_PROVIDERS.GNAME]: AccessConfigFieldsProviderGname,
[ACCESS_PROVIDERS.GODADDY]: AccessConfigFieldsProviderGoDaddy,
[ACCESS_PROVIDERS.GOEDGE]: AccessConfigFieldsProviderGoEdge,
[ACCESS_PROVIDERS.GLOBALSIGNATLAS]: AccessConfigFieldsProviderGlobalSignAtlas,
[ACCESS_PROVIDERS.GOOGLETRUSTSERVICES]: AccessConfigFieldsProviderGoogleTrustServices,
[ACCESS_PROVIDERS.HETZNER]: AccessConfigFieldsProviderHetzner,
[ACCESS_PROVIDERS.HOSTINGDE]: AccessConfigFieldsProviderHostingde,
[ACCESS_PROVIDERS.HOSTINGER]: AccessConfigFieldsProviderHostinger,
[ACCESS_PROVIDERS.HUAWEICLOUD]: AccessConfigFieldsProviderHuaweiCloud,
[ACCESS_PROVIDERS.IONOS]: AccessConfigFieldsProviderIONOS,
[ACCESS_PROVIDERS.JDCLOUD]: AccessConfigFieldsProviderJDCloud,
[ACCESS_PROVIDERS.KONG]: AccessConfigFieldsProviderKong,
[ACCESS_PROVIDERS.KUBERNETES]: AccessConfigFieldsProviderKubernetes,
[ACCESS_PROVIDERS.KSYUN]: AccessConfigFieldsProviderKsyun,
[ACCESS_PROVIDERS.LARKBOT]: AccessConfigFieldsProviderLarkBot,
[ACCESS_PROVIDERS.LECDN]: AccessConfigFieldsProviderLeCDN,
[ACCESS_PROVIDERS.INFOMANIAK]: AccessConfigFieldsProviderInfomaniak,
[ACCESS_PROVIDERS.LINODE]: AccessConfigFieldsProviderLinode,
[ACCESS_PROVIDERS.LITESSL]: AccessConfigFieldsProviderLiteSSL,
[ACCESS_PROVIDERS.MATTERMOST]: AccessConfigFieldsProviderMattermost,
[ACCESS_PROVIDERS.MOHUA]: AccessConfigFieldsProviderMohua,
[ACCESS_PROVIDERS.NAMECHEAP]: AccessConfigFieldsProviderNamecheap,
[ACCESS_PROVIDERS.NAMEDOTCOM]: AccessConfigFieldsProviderNameDotCom,
[ACCESS_PROVIDERS.NAMESILO]: AccessConfigFieldsProviderNameSilo,
[ACCESS_PROVIDERS.NETCUP]: AccessConfigFieldsProviderNetcup,
[ACCESS_PROVIDERS.NETLIFY]: AccessConfigFieldsProviderNetlify,
[ACCESS_PROVIDERS.NGINXPROXYMANAGER]: AccessConfigFieldsProviderNginxProxyManager,
[ACCESS_PROVIDERS.NS1]: AccessConfigFieldsProviderNS1,
[ACCESS_PROVIDERS.OVHCLOUD]: AccessConfigFieldsProviderOVHcloud,
[ACCESS_PROVIDERS.PORKBUN]: AccessConfigFieldsProviderPorkbun,
[ACCESS_PROVIDERS.POWERDNS]: AccessConfigFieldsProviderPowerDNS,
[ACCESS_PROVIDERS.PROXMOXVE]: AccessConfigFieldsProviderProxmoxVE,
[ACCESS_PROVIDERS.QINGCLOUD]: AccessConfigFieldsProviderQingCloud,
[ACCESS_PROVIDERS.QINIU]: AccessConfigFieldsProviderQiniu,
[ACCESS_PROVIDERS.RAINYUN]: AccessConfigFieldsProviderRainYun,
[ACCESS_PROVIDERS.RATPANEL]: AccessConfigFieldsProviderRatPanel,
[ACCESS_PROVIDERS.RFC2136]: AccessConfigFieldsProviderRFC2136,
[ACCESS_PROVIDERS.S3]: AccessConfigFieldsProviderS3,
[ACCESS_PROVIDERS.SAFELINE]: AccessConfigFieldsProviderSafeLine,
[ACCESS_PROVIDERS.SECTIGO]: AccessConfigFieldsProviderSectigo,
[ACCESS_PROVIDERS.SLACKBOT]: AccessConfigFieldsProviderSlackBot,
[ACCESS_PROVIDERS.SPACESHIP]: AccessConfigFieldsProviderSpaceship,
[ACCESS_PROVIDERS.SSLCOM]: AccessConfigFieldsProviderSSLCom,
[ACCESS_PROVIDERS.SSH]: AccessConfigFieldsProviderSSH,
[ACCESS_PROVIDERS.SYNOLOGYDSM]: AccessConfigFieldsProviderSynologyDSM,
[ACCESS_PROVIDERS.TECHNITIUMDNS]: AccessConfigFieldsProviderTechnitiumDNS,
[ACCESS_PROVIDERS.TELEGRAMBOT]: AccessConfigFieldsProviderTelegramBot,
[ACCESS_PROVIDERS.TENCENTCLOUD]: AccessConfigFieldsProviderTencentCloud,
[ACCESS_PROVIDERS.TODAYNIC]: AccessConfigFieldsProviderTodayNIC,
[ACCESS_PROVIDERS.UCLOUD]: AccessConfigFieldsProviderUCloud,
[ACCESS_PROVIDERS.UNICLOUD]: AccessConfigFieldsProviderUniCloud,
[ACCESS_PROVIDERS.UPYUN]: AccessConfigFieldsProviderUpyun,
[ACCESS_PROVIDERS.VERCEL]: AccessConfigFieldsProviderVercel,
[ACCESS_PROVIDERS.VOLCENGINE]: AccessConfigFieldsProviderVolcEngine,
[ACCESS_PROVIDERS.VULTR]: AccessConfigFieldsProviderVultr,
[ACCESS_PROVIDERS.WANGSU]: AccessConfigFieldsProviderWangsu,
[ACCESS_PROVIDERS.WEBHOOK]: AccessConfigFieldsProviderWebhook,
[ACCESS_PROVIDERS.WECOMBOT]: AccessConfigFieldsProviderWeComBot,
[ACCESS_PROVIDERS.WESTCN]: AccessConfigFieldsProviderWestcn,
[ACCESS_PROVIDERS.XINNET]: AccessConfigFieldsProviderXinnet,
[ACCESS_PROVIDERS.ZEROSSL]: AccessConfigFieldsProviderZeroSSL,
};
const useComponent = (provider: string, { initProps, deps = [] }: { initProps?: (provider: string) => any; deps?: unknown[] }) => {
const initComponent = () => {
const Component = providerComponentMap[provider as AccessProviderType];
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/access/forms/AccessConfigFieldsProvider1Panel.tsx
================================================
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input, Select, Switch } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { useFormNestedFieldsContext } from "./_context";
const AccessConfigFormFieldsProvider1Panel = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const initialValues = getInitialValues();
return (
<>
({ label: s, value: s }))} placeholder={t("access.form.1panel_api_version.placeholder")} />
}
>
>
);
};
const getInitialValues = (): Nullish>> => {
return {
serverUrl: "http://:20410/",
apiVersion: "v1",
apiKey: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
serverUrl: z.url(t("common.errmsg.url_invalid")),
apiVersion: z.string().nonempty(t("access.form.1panel_api_version.placeholder")),
apiKey: z.string().nonempty(t("access.form.1panel_api_key.placeholder")),
allowInsecureConnections: z.boolean().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProvider1Panel, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProvider35cn.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 AccessConfigFormFieldsProvider35cn = () => {
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: "",
apiPassword: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
username: z.string().nonempty(t("access.form.35cn_username.placeholder")),
apiPassword: z.string().nonempty(t("access.form.35cn_api_password.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProvider35cn, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProvider51DNScom.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 AccessConfigFormFieldsProvider51DNScom = () => {
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.51dnscom_api_key.placeholder")),
apiSecret: z.string().nonempty(t("access.form.51dnscom_api_secret.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProvider51DNScom, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderACMECA.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 AccessConfigFormFieldsProviderACMECA = () => {
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 {
endpoint: "https://example.com/acme/directory",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
endpoint: z.url(t("common.errmsg.url_invalid")),
eabKid: z.string().nullish(),
eabHmacKey: z.string().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderACMECA, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderACMEDNS.tsx
================================================
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod/v4";
import FileTextInput from "@/components/FileTextInput";
import { isJsonObject } from "@/utils/validator";
import { useFormNestedFieldsContext } from "./_context";
const AccessConfigFieldsProviderACMEDNS = () => {
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: "https://auth.acme-dns.io/",
credentials: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
serverUrl: z.url(t("common.errmsg.url_invalid")),
credentials: z.string().refine((v) => isJsonObject(v), t("common.errmsg.json_invalid")),
});
};
const _default = Object.assign(AccessConfigFieldsProviderACMEDNS, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderACMEHttpReq.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 AccessConfigFormFieldsProviderACMEHttpReq = () => {
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 {
endpoint: "https://example.com/api/",
mode: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
endpoint: z.url(t("common.errmsg.url_invalid")),
mode: z.string().nullish(),
username: z.string().nullish(),
password: z.string().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderACMEHttpReq, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderAPISIX.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 AccessConfigFormFieldsProviderAPISIX = () => {
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://:9180/",
apiKey: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
serverUrl: z.url(t("common.errmsg.url_invalid")),
apiKey: z.string().nonempty(t("access.form.apisix_api_key.placeholder")),
allowInsecureConnections: z.boolean().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderAPISIX, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderAWS.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 AccessConfigFormFieldsProviderAWS = () => {
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.aws_access_key_id.placeholder")),
secretAccessKey: z.string().nonempty(t("access.form.aws_secret_access_key.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderAWS, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderActalisSSL.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 AccessConfigFormFieldsProviderActalisSSL = () => {
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(AccessConfigFormFieldsProviderActalisSSL, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderAkamai.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 AccessConfigFormFieldsProviderAkamai = () => {
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 {
host: "",
clientToken: "",
clientSecret: "",
accessToken: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
host: z.string().nonempty(t("access.form.akamai_host.placeholder")),
clientToken: z.string().nonempty(t("access.form.akamai_client_token.placeholder")),
clientSecret: z.string().nonempty(t("access.form.akamai_client_secret.placeholder")),
accessToken: z.string().nonempty(t("access.form.akamai_access_token.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderAkamai, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderAliyun.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 AccessConfigFormFieldsProviderAliyun = () => {
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: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
accessKeyId: z.string().nonempty(t("access.form.aliyun_access_key_id.placeholder")),
accessKeySecret: z.string().nonempty(t("access.form.aliyun_access_key_secret.placeholder")),
resourceGroupId: z.string().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderAliyun, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderArvanCloud.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 AccessConfigFormFieldsProviderArvanCloud = () => {
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.arvancloud_api_key.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderArvanCloud, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderAzure.tsx
================================================
import { getI18n, useTranslation } from "react-i18next";
import { AutoComplete, Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { matchSearchOption } from "@/utils/search";
import { useFormNestedFieldsContext } from "./_context";
const AccessConfigFormFieldsProviderAzure = () => {
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("access.form.azure_cloud_name.placeholder")}
showSearch={{
filterOption: (inputValue, option) => matchSearchOption(inputValue, option!),
}}
/>
>
);
};
const getInitialValues = (): Nullish>> => {
return {
tenantId: "",
clientId: "",
clientSecret: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
tenantId: z.string().nonempty(t("access.form.azure_tenant_id.placeholder")),
clientId: z.string().nonempty(t("access.form.azure_client_id.placeholder")),
clientSecret: z.string().nonempty(t("access.form.azure_client_secret.placeholder")),
subscriptionId: z.string().nullish(),
resourceGroupName: z.string().nullish(),
cloudName: z.string().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderAzure, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderBaiduCloud.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 AccessConfigFormFieldsProviderBaiduCloud = () => {
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.baiducloud_access_key_id.placeholder")),
secretAccessKey: z.string().nonempty(t("access.form.baiducloud_secret_access_key.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderBaiduCloud, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderBaishan.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 AccessConfigFormFieldsProviderBaishan = () => {
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 {
apiToken: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
apiToken: z.string().nonempty(t("access.form.baishan_api_token.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderBaishan, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderBaotaPanel.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 AccessConfigFormFieldsProviderBaotaPanel = () => {
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://:8888/",
apiKey: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
serverUrl: z.url(t("common.errmsg.url_invalid")),
apiKey: z.string().nonempty(t("access.form.baotapanel_api_key.placeholder")),
allowInsecureConnections: z.boolean().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderBaotaPanel, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderBaotaPanelGo.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 AccessConfigFormFieldsProviderBaotaPanelGo = () => {
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://:8888/",
apiKey: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
serverUrl: z.url(t("common.errmsg.url_invalid")),
apiKey: z.string().nonempty(t("access.form.baotapanelgo_api_key.placeholder")),
allowInsecureConnections: z.boolean().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderBaotaPanelGo, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderBaotaWAF.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 AccessConfigFormFieldsProviderBaotaWAF = () => {
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://:8379/",
apiKey: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
serverUrl: z.url(t("common.errmsg.url_invalid")),
apiKey: z.string().nonempty(t("access.form.baotawaf_api_key.placeholder")),
allowInsecureConnections: z.boolean().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderBaotaWAF, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderBookMyName.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 AccessConfigFormFieldsProviderBookMyName = () => {
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.bookmyname_username.placeholder")),
password: z.string().nonempty(t("access.form.bookmyname_password.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderBookMyName, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderBunny.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 AccessConfigFormFieldsProviderBunny = () => {
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.bunny_api_key.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderBunny, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderBytePlus.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 AccessConfigFormFieldsProviderBytePlus = () => {
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 {
accessKey: "",
secretKey: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
accessKey: z.string().nonempty(t("access.form.byteplus_access_key.placeholder")),
secretKey: z.string().nonempty(t("access.form.byteplus_secret_key.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderBytePlus, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderCMCCCloud.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 AccessConfigFormFieldsProviderCMCCCloud = () => {
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: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
accessKeyId: z.string().nonempty(t("access.form.cmcccloud_access_key_id.placeholder")),
accessKeySecret: z.string().nonempty(t("access.form.cmcccloud_access_key_secret.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderCMCCCloud, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderCPanel.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 AccessConfigFormFieldsProviderCPanel = () => {
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://:2082/",
username: "",
apiToken: "",
};
};
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.cpanel_username.placeholder")),
apiToken: z.string().nonempty(t("access.form.cpanel_api_token.placeholder")),
allowInsecureConnections: z.boolean().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderCPanel, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderCTCCCloud.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 AccessConfigFormFieldsProviderCTCCCloud = () => {
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.ctcccloud_access_key_id.placeholder")),
secretAccessKey: z.string().nonempty(t("access.form.ctcccloud_secret_access_key.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderCTCCCloud, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderCacheFly.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 AccessConfigFormFieldsProviderCacheFly = () => {
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 {
apiToken: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
apiToken: z.string().nonempty(t("access.form.cachefly_api_token.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderCacheFly, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderCdnfly.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 AccessConfigFormFieldsProviderCdnfly = () => {
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://:88/",
apiKey: "",
apiSecret: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
serverUrl: z.url(t("common.errmsg.url_invalid")),
apiKey: z.string().nonempty(t("access.form.cdnfly_api_key.placeholder")),
apiSecret: z.string().nonempty(t("access.form.cdnfly_api_secret.placeholder")),
allowInsecureConnections: z.boolean().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderCdnfly, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderClouDNS.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 AccessConfigFormFieldsProviderClouDNS = () => {
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 {
authId: "",
authPassword: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
authId: z.string().nonempty(t("access.form.cloudns_auth_id.placeholder")),
authPassword: z.string().nonempty(t("access.form.cloudns_auth_password.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderClouDNS, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderCloudflare.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 AccessConfigFormFieldsProviderCloudflare = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const initialValues = getInitialValues();
return (
<>
}
>
}
rules={[formRule]}
tooltip={ }
>
>
);
};
const getInitialValues = (): Nullish>> => {
return {
dnsApiToken: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
dnsApiToken: z.string().nonempty(t("access.form.cloudflare_dns_api_token.placeholder")),
zoneApiToken: z.string().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderCloudflare, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderConstellix.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 AccessConfigFormFieldsProviderConstellix = () => {
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: "",
secretKey: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
apiKey: z.string().nonempty(t("access.form.constellix_api_key.placeholder")),
secretKey: z.string().nonempty(t("access.form.constellix_secret_key.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderConstellix, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderDNSExit.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 AccessConfigFormFieldsProviderDNSExit = () => {
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.dnsexit_api_key.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderDNSExit, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderDNSLA.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 AccessConfigFormFieldsProviderDNSLA = () => {
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 {
apiId: "",
apiSecret: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
apiId: z.string().nonempty(t("access.form.dnsla_api_id.placeholder")),
apiSecret: z.string().nonempty(t("access.form.dnsla_api_secret.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderDNSLA, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderDNSMadeEasy.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 AccessConfigFormFieldsProviderDNSMadeEasy = () => {
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.dnsmadeeasy_api_key.placeholder")),
apiSecret: z.string().nonempty(t("access.form.dnsmadeeasy_api_secret.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderDNSMadeEasy, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderDeSEC.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 AccessConfigFormFieldsProviderDeSEC = () => {
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 {
token: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
token: z.string().nonempty(t("access.form.desec_token.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderDeSEC, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderDigiCert.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 AccessConfigFormFieldsProviderDigiCert = () => {
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(AccessConfigFormFieldsProviderDigiCert, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderDigitalOcean.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 AccessConfigFormFieldsProviderDigitalOcean = () => {
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 {
accessToken: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
accessToken: z.string().nonempty(t("access.form.digitalocean_access_token.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderDigitalOcean, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderDingTalkBot.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 AccessConfigFormFieldsProviderDingTalkBot = () => {
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.dingtalkbot_custom_payload.checkbox")}
>
);
};
const getInitialValues = (): Nullish>> => {
return {
webhookUrl: "",
secret: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z
.object({
webhookUrl: z.url(t("common.errmsg.url_invalid")),
secret: z.string().nullish(),
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(AccessConfigFormFieldsProviderDingTalkBot, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderDiscordBot.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 AccessConfigFormFieldsProviderDiscordBot = () => {
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.discordbot_token.placeholder")),
channelId: z.string().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderDiscordBot, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderDogeCloud.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 AccessConfigFormFieldsProviderDogeCloud = () => {
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 {
accessKey: "",
secretKey: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
accessKey: z.string().nonempty(t("access.form.dogecloud_access_key.placeholder")),
secretKey: z.string().nonempty(t("access.form.dogecloud_secret_key.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderDogeCloud, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderDokploy.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 AccessConfigFormFieldsProviderDokploy = () => {
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://:3000/",
apiKey: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
serverUrl: z.url(t("common.errmsg.url_invalid")),
apiKey: z.string().nonempty(t("access.form.dokploy_api_key.placeholder")),
allowInsecureConnections: z.boolean().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderDokploy, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderDuckDNS.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 AccessConfigFormFieldsProviderDuckDNS = () => {
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 {
token: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
token: z.string().nonempty(t("access.form.duckdns_token.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderDuckDNS, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderDynu.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 AccessConfigFormFieldsProviderDynu = () => {
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.dynu_api_key.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderDynu, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderDynv6.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 AccessConfigFormFieldsProviderDynv6 = () => {
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 {
httpToken: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
httpToken: z.string().nonempty(t("access.form.dynv6_http_token.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderDynv6, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderEmail.tsx
================================================
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input, InputNumber, Select, Switch } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Show from "@/components/Show";
import { isEmail, isHostname, isPortNumber } from "@/utils/validator";
import { useFormNestedFieldsContext } from "./_context";
const AccessConfigFormFieldsProviderEmail = () => {
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 fieldSmtpTls = Form.useWatch([parentNamePath, "smtpTls"], formInst);
return (
<>
{t("access.form.email_smtp_tls.option.true.label")}
{t("access.form.email_smtp_tls.option.false.label")}
>
);
};
const getInitialValues = (): Nullish>> => {
return {
smtpHost: "",
smtpPort: 465,
smtpTls: true,
username: "",
password: "",
senderAddress: "",
senderName: "",
receiverAddress: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
smtpHost: z.string().refine((v) => isHostname(v), t("common.errmsg.host_invalid")),
smtpPort: z.coerce.number().refine((v) => isPortNumber(v), t("common.errmsg.port_invalid")),
smtpTls: z.boolean().nullish(),
username: z.string().nonempty(t("access.form.email_username.placeholder")),
password: z.string().nonempty(t("access.form.email_password.placeholder")),
senderAddress: z.email(t("common.errmsg.email_invalid")),
senderName: z.string().nullish(),
receiverAddress: z
.string()
.nullish()
.refine((v) => {
if (!v) return true;
return isEmail(v);
}, t("common.errmsg.email_invalid")),
allowInsecureConnections: z.boolean().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderEmail, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderFlexCDN.tsx
================================================
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input, Radio, Switch } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { useFormNestedFieldsContext } from "./_context";
const AccessConfigFormFieldsProviderFlexCDN = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const initialValues = getInitialValues();
return (
<>
({ label: t(`access.form.flexcdn_api_role.option.${s}.label`), value: s }))} />
}
>
}
>
>
);
};
const getInitialValues = (): Nullish>> => {
return {
serverUrl: "http://:8000/",
apiRole: "user",
accessKeyId: "",
accessKey: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
serverUrl: z.url(t("common.errmsg.url_invalid")),
apiRole: z.literal(["user", "admin"], t("access.form.flexcdn_api_role.placeholder")),
accessKeyId: z.string().nonempty(t("access.form.flexcdn_access_key_id.placeholder")),
accessKey: z.string().nonempty(t("access.form.flexcdn_access_key.placeholder")),
allowInsecureConnections: z.boolean().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderFlexCDN, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderFlyIO.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 AccessConfigFormFieldsProviderFlyIO = () => {
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 {
apiToken: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
apiToken: z.string().nonempty(t("access.form.flyio_api_token.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderFlyIO, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderGandinet.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 AccessConfigFormFieldsProviderGandinet = () => {
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 {
personalAccessToken: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
personalAccessToken: z.string().nonempty(t("access.form.gandinet_personal_access_token.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderGandinet, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderGcore.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 AccessConfigFormFieldsProviderGcore = () => {
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 {
apiToken: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
apiToken: z.string().nonempty(t("access.form.gcore_api_token.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderGcore, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderGlobalSignAtlas.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 AccessConfigFormFieldsProviderGobalSignAtlas = () => {
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(AccessConfigFormFieldsProviderGobalSignAtlas, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderGname.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 AccessConfigFormFieldsProviderGname = () => {
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 {
appId: "",
appKey: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
appId: z.string().nonempty(t("access.form.gname_app_id.placeholder")),
appKey: z.string().nonempty(t("access.form.gname_app_key.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderGname, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderGoDaddy.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 AccessConfigFormFieldsProviderGoDaddy = () => {
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.godaddy_api_key.placeholder")),
apiSecret: z.string().nonempty(t("access.form.godaddy_api_secret.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderGoDaddy, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderGoEdge.tsx
================================================
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input, Radio, Switch } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { useFormNestedFieldsContext } from "./_context";
const AccessConfigFormFieldsProviderGoEdge = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const initialValues = getInitialValues();
return (
<>
({ label: t(`access.form.goedge_api_role.option.${s}.label`), value: s }))} />
}
>
}
>
>
);
};
const getInitialValues = (): Nullish>> => {
return {
serverUrl: "http://:7788/",
apiRole: "user",
accessKeyId: "",
accessKey: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
serverUrl: z.url(t("common.errmsg.url_invalid")),
apiRole: z.literal(["user", "admin"], t("access.form.goedge_api_role.placeholder")),
accessKeyId: z.string().nonempty(t("access.form.goedge_access_key_id.placeholder")),
accessKey: z.string().nonempty(t("access.form.goedge_access_key.placeholder")),
allowInsecureConnections: z.boolean().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderGoEdge, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderGoogleTrustServices.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 AccessConfigFormFieldsProviderGoogleTrustServices = () => {
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(AccessConfigFormFieldsProviderGoogleTrustServices, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderHetzner.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 AccessConfigFormFieldsProviderHetzner = () => {
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 {
apiToken: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
apiToken: z.string().nonempty(t("access.form.hetzner_api_token.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderHetzner, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderHostingde.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 AccessConfigFormFieldsProviderHostingde = () => {
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.hostingde_api_key.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderHostingde, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderHostinger.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 AccessConfigFormFieldsProviderHostinger = () => {
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 {
apiToken: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
apiToken: z.string().nonempty(t("access.form.hostinger_api_token.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderHostinger, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderHuaweiCloud.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 AccessConfigFormFieldsProviderHuaweiCloud = () => {
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.huaweicloud_access_key_id.placeholder")),
secretAccessKey: z.string().nonempty(t("access.form.huaweicloud_secret_access_key.placeholder")),
enterpriseProjectId: z.string().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderHuaweiCloud, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderIONOS.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 AccessConfigFormFieldsProviderIONOS = () => {
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 {
apiKeyPublicPrefix: "",
apiKeySecret: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
apiKeyPublicPrefix: z.string().nonempty(t("access.form.ionos_api_key_public_prefix.placeholder")),
apiKeySecret: z.string().nonempty(t("access.form.ionos_api_key_secret.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderIONOS, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderInfomaniak.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 AccessConfigFormFieldsProviderInfomaniak = () => {
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 {
accessToken: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
accessToken: z.string().nonempty(t("access.form.infomaniak_access_token.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderInfomaniak, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderJDCloud.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 AccessConfigFormFieldsProviderJDCloud = () => {
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: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
accessKeyId: z.string().nonempty(t("access.form.jdcloud_access_key_id.placeholder")),
accessKeySecret: z.string().nonempty(t("access.form.jdcloud_access_key_secret.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderJDCloud, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderKong.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 AccessConfigFormFieldsProviderKong = () => {
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://:8001/",
apiToken: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
serverUrl: z.url(t("common.errmsg.url_invalid")),
apiToken: z.string().nullish(),
allowInsecureConnections: z.boolean().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderKong, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderKsyun.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 AccessConfigFormFieldsProviderKsyun = () => {
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.ksyun_access_key_id.placeholder")),
secretAccessKey: z.string().nonempty(t("access.form.ksyun_secret_access_key.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderKsyun, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderKubernetes.tsx
================================================
import { getI18n, useTranslation } from "react-i18next";
import { Form } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import FileTextInput from "@/components/FileTextInput";
import { useFormNestedFieldsContext } from "./_context";
const AccessConfigFormFieldsProviderKubernetes = () => {
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({
kubeConfig: z
.string()
.max(20480, t("common.errmsg.string_max", { max: 20480 }))
.nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderKubernetes, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderLarkBot.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 AccessConfigFormFieldsProviderLarkBot = () => {
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.larkbot_custom_payload.checkbox")}
>
);
};
const getInitialValues = (): Nullish>> => {
return {
webhookUrl: "",
secret: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z
.object({
webhookUrl: z.url(t("common.errmsg.url_invalid")),
secret: z.string().nullish(),
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(
{
msg_type: "text",
content: {
text: "${CERTIMATE_NOTIFIER_SUBJECT}\n\n${CERTIMATE_NOTIFIER_MESSAGE}",
},
},
null,
2
);
const _default = Object.assign(AccessConfigFormFieldsProviderLarkBot, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderLeCDN.tsx
================================================
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input, Radio, Select, Switch } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { useFormNestedFieldsContext } from "./_context";
const AccessConfigFormFieldsProviderLeCDN = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const initialValues = getInitialValues();
return (
<>
({ label: s, value: s }))} placeholder={t("access.form.lecdn_api_version.placeholder")} />
({ label: t(`access.form.lecdn_api_role.option.${s}.label`), value: s }))} />
>
);
};
const getInitialValues = (): Nullish>> => {
return {
serverUrl: "http://:5090/",
apiVersion: "v3",
apiRole: "client",
username: "",
password: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
serverUrl: z.url(t("common.errmsg.url_invalid")),
apiVersion: z.literal(["v3"], t("access.form.lecdn_api_version.placeholder")),
apiRole: z.literal(["client", "master"], t("access.form.lecdn_api_role.placeholder")),
username: z.string().nonempty(t("access.form.lecdn_username.placeholder")),
password: z.string().nonempty(t("access.form.lecdn_password.placeholder")),
allowInsecureConnections: z.boolean().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderLeCDN, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderLinode.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 AccessConfigFormFieldsProviderLinode = () => {
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 {
accessToken: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
accessToken: z.string().nonempty(t("access.form.linode_access_token.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderLinode, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderLiteSSL.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 AccessConfigFormFieldsProviderLiteSSL = () => {
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(AccessConfigFormFieldsProviderLiteSSL, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderMattermost.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 AccessConfigFormFieldsProviderMattermost = () => {
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://:8065/",
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.mattermost_username.placeholder")),
password: z.string().nonempty(t("access.form.mattermost_password.placeholder")),
channelId: z.string().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderMattermost, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderMohua.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 AccessConfigFormFieldsProviderMohua = () => {
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: "",
apiPassword: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
username: z.string().nonempty(t("access.form.mohua_username.placeholder")),
apiPassword: z.string().nonempty(t("access.form.mohua_api_password.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderMohua, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderNS1.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 AccessConfigFormFieldsProviderNS1 = () => {
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.ns1_api_key.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderNS1, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderNameDotCom.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 AccessConfigFormFieldsProviderNameDotCom = () => {
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: "",
apiToken: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
username: z.string().nonempty(t("access.form.namedotcom_username.placeholder")),
apiToken: z.string().nonempty(t("access.form.namedotcom_api_token.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderNameDotCom, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderNameSilo.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 AccessConfigFormFieldsProviderNameSilo = () => {
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.namesilo_api_key.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderNameSilo, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderNamecheap.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 AccessConfigFormFieldsProviderNamecheap = () => {
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: "",
apiKey: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
username: z.string().nonempty(t("access.form.namecheap_username.placeholder")),
apiKey: z.string().nonempty(t("access.form.namecheap_api_key.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderNamecheap, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderNetcup.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 AccessConfigFormFieldsProviderNetcup = () => {
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 {
customerNumber: "",
apiKey: "",
apiPassword: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
customerNumber: z.string().nonempty(t("access.form.netcup_customer_number.placeholder")),
apiKey: z.string().nonempty(t("access.form.netcup_api_key.placeholder")),
apiPassword: z.string().nonempty(t("access.form.netcup_api_password.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderNetcup, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderNetlify.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 AccessConfigFormFieldsProviderNetlify = () => {
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 {
apiToken: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
apiToken: z.string().nonempty(t("access.form.netlify_api_token.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderNetlify, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderNginxProxyManager.tsx
================================================
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input, Radio, Switch } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Show from "@/components/Show";
import { useFormNestedFieldsContext } from "./_context";
const AUTH_METHOD_PASSWORD = "password" as const;
const AUTH_METHOD_TOKEN = "token" as const;
const AccessConfigFormFieldsProviderNginxProxyManager = () => {
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 fieldAuthMethod = Form.useWatch([parentNamePath, "authMethod"], formInst);
return (
<>
{t("access.form.nginxproxymanager_auth_method.option.password.label")}
{t("access.form.nginxproxymanager_auth_method.option.token.label")}
>
);
};
const getInitialValues = (): Nullish>> => {
return {
serverUrl: "http://:81/",
authMethod: AUTH_METHOD_PASSWORD,
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z
.object({
serverUrl: z.url(t("common.errmsg.url_invalid")),
authMethod: z.literal([AUTH_METHOD_PASSWORD, AUTH_METHOD_TOKEN], t("access.form.nginxproxymanager_auth_method.placeholder")),
username: z.string().nullish(),
password: z.string().nullish(),
apiToken: z.string().nullish(),
allowInsecureConnections: z.boolean().nullish(),
})
.superRefine((values, ctx) => {
switch (values.authMethod) {
case AUTH_METHOD_PASSWORD:
{
if (!values.username?.trim()) {
ctx.addIssue({
code: "custom",
message: t("access.form.nginxproxymanager_username.placeholder"),
path: ["username"],
});
}
if (!values.password?.trim()) {
ctx.addIssue({
code: "custom",
message: t("access.form.nginxproxymanager_password.placeholder"),
path: ["password"],
});
}
}
break;
case AUTH_METHOD_TOKEN:
{
if (!values.apiToken?.trim()) {
ctx.addIssue({
code: "custom",
message: t("access.form.nginxproxymanager_api_token.placeholder"),
path: ["apiToken"],
});
}
}
break;
}
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderNginxProxyManager, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderOVHcloud.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 { matchSearchOption } from "@/utils/search";
import { useFormNestedFieldsContext } from "./_context";
const AUTH_METHOD_APPLICATION = "application" as const;
const AUTH_METHOD_OAUTH2 = "oauth2" as const;
const AccessConfigFormFieldsProviderOVHcloud = () => {
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 fieldAuthMethod = Form.useWatch([parentNamePath, "authMethod"], formInst);
return (
<>
({ value }))}
placeholder={t("access.form.ovhcloud_endpoint.placeholder")}
showSearch={{
filterOption: (inputValue, option) => matchSearchOption(inputValue, option!),
}}
/>
{t("access.form.ovhcloud_auth_method.option.application.label")}
{t("access.form.ovhcloud_auth_method.option.oauth2.label")}
}
>
}
>
}
>
}
>
}
>
>
);
};
const getInitialValues = (): Nullish>> => {
return {
endpoint: "ovh-eu",
authMethod: AUTH_METHOD_APPLICATION,
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z
.object({
endpoint: z.string().nonempty(t("access.form.ovhcloud_endpoint.placeholder")),
authMethod: z.literal([AUTH_METHOD_APPLICATION, AUTH_METHOD_OAUTH2], t("access.form.ovhcloud_auth_method.placeholder")),
applicationKey: z.string().nullish(),
applicationSecret: z.string().nullish(),
consumerKey: z.string().nullish(),
clientId: z.string().nullish(),
clientSecret: z.string().nullish(),
})
.superRefine((values, ctx) => {
switch (values.authMethod) {
case AUTH_METHOD_APPLICATION:
{
if (!values.applicationKey?.trim()) {
ctx.addIssue({
code: "custom",
message: t("access.form.ovhcloud_application_key.placeholder"),
path: ["applicationKey"],
});
}
if (!values.applicationSecret?.trim()) {
ctx.addIssue({
code: "custom",
message: t("access.form.ovhcloud_application_secret.placeholder"),
path: ["applicationSecret"],
});
}
if (!values.consumerKey?.trim()) {
ctx.addIssue({
code: "custom",
message: t("access.form.ovhcloud_consumer_key.placeholder"),
path: ["consumerKey"],
});
}
}
break;
case AUTH_METHOD_OAUTH2:
{
if (!values.clientId?.trim()) {
ctx.addIssue({
code: "custom",
message: t("access.form.ovhcloud_client_id.placeholder"),
path: ["clientId"],
});
}
if (!values.clientSecret?.trim()) {
ctx.addIssue({
code: "custom",
message: t("access.form.ovhcloud_client_secret.placeholder"),
path: ["clientSecret"],
});
}
}
break;
}
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderOVHcloud, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderPorkbun.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 AccessConfigFormFieldsProviderPorkbun = () => {
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: "",
secretApiKey: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
apiKey: z.string().nonempty(t("access.form.porkbun_api_key.placeholder")),
secretApiKey: z.string().nonempty(t("access.form.porkbun_secret_api_key.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderPorkbun, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderPowerDNS.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 AccessConfigFormFieldsProviderPowerDNS = () => {
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://:8082/",
apiKey: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
serverUrl: z.url(t("common.errmsg.url_invalid")),
apiKey: z.string().nonempty(t("access.form.powerdns_api_key.placeholder")),
allowInsecureConnections: z.boolean().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderPowerDNS, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderProxmoxVE.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 AccessConfigFormFieldsProviderProxmoxVE = () => {
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://:8006/",
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.proxmoxve_api_token.placeholder")),
apiTokenSecret: z.string().nullish(),
allowInsecureConnections: z.boolean().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderProxmoxVE, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderQingCloud.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 AccessConfigFormFieldsProviderQingCloud = () => {
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.qingcloud_access_key_id.placeholder")),
secretAccessKey: z.string().nonempty(t("access.form.qingcloud_secret_access_key.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderQingCloud, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderQiniu.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 AccessConfigFormFieldsProviderQiniu = () => {
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 {
accessKey: "",
secretKey: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
accessKey: z.string().nonempty(t("access.form.qiniu_access_key.placeholder")),
secretKey: z.string().nonempty(t("access.form.qiniu_secret_key.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderQiniu, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderRFC2136.tsx
================================================
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input, InputNumber, Select } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { isHostname, isPortNumber } from "@/utils/validator";
import { useFormNestedFieldsContext } from "./_context";
const AccessConfigFormFieldsProviderRFC2136 = () => {
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 {
host: "127.0.0.1",
port: 53,
tsigAlgorithm: "hmac-sha1.",
tsigKey: "",
tsigSecret: "",
};
};
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")),
tsigAlgorithm: z.string().nonempty(t("access.form.rfc2136_tsig_algorithm.placeholder")),
tsigKey: z.string().nullish(),
tsigSecret: z.string().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderRFC2136, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderRainYun.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 AccessConfigFormFieldsProviderRainYun = () => {
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.rainyun_api_key.placeholder")),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderRainYun, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderRatPanel.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 AccessConfigFormFieldsProviderRatPanel = () => {
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://:8888/",
accessTokenId: 1,
accessToken: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
serverUrl: z.url(t("common.errmsg.url_invalid")),
accessTokenId: z.coerce.number().int().positive(),
accessToken: z.string().nonempty(t("access.form.ratpanel_access_token.placeholder")),
allowInsecureConnections: z.boolean().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderRatPanel, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderS3.tsx
================================================
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input, Select, Switch } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { isHostname, isUrlWithHttpOrHttps } from "@/utils/validator";
import { useFormNestedFieldsContext } from "./_context";
const AccessConfigFieldsProviderS3 = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const initialValues = getInitialValues();
return (
<>
}
rules={[formRule]}
>
({ label: s, value: s }))} placeholder={t("access.form.s3_signature_version.placeholder")} />
}
>
>
);
};
const getInitialValues = (): Nullish>> => {
return {
endpoint: "",
accessKey: "",
secretKey: "",
signatureVersion: "v4",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
const { t } = i18n;
return z.object({
endpoint: z.string().refine((v) => isHostname(v) || isUrlWithHttpOrHttps(v), t("access.form.s3_endpoint.placeholder")),
accessKey: z.string().nonempty(t("access.form.s3_access_key.placeholder")),
secretKey: z.string().nonempty(t("access.form.s3_secret_key.placeholder")),
signatureVersion: z.enum(["v2", "v4"]),
usePathStyle: z.boolean().nullish(),
allowInsecureConnections: z.boolean().nullish(),
});
};
const _default = Object.assign(AccessConfigFieldsProviderS3, {
getInitialValues,
getSchema,
});
export default _default;
================================================
FILE: ui/src/components/access/forms/AccessConfigFieldsProviderSSH.tsx
================================================
import { getI18n, useTranslation } from "react-i18next";
import { IconCircleArrowDown, IconCircleArrowUp, IconCircleMinus, IconCirclePlus } from "@tabler/icons-react";
import { Button, Collapse, Form, Input, InputNumber, Radio } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import FileTextInput from "@/components/FileTextInput";
import Show from "@/components/Show";
import { mergeCls } from "@/utils/css";
import { isHostname, isPortNumber } from "@/utils/validator";
import { useFormNestedFieldsContext } from "./_context";
const AUTH_METHOD_NONE = "none" as const;
const AUTH_METHOD_PASSWORD = "password" as const;
const AUTH_METHOD_KEY = "key" as const;
const AccessConfigFormFieldsProviderSSH = ({ disabled }: { disabled?: boolean }) => {
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 fieldAuthMethod = Form.useWatch([parentNamePath, "authMethod"], formInst);
const fieldJumpServers = Form.useWatch([parentNamePath, "jumpServers"], formInst);
return (
<>