diff --git a/README.md b/README.md index ed79b12f..745cec19 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,7 @@ make local.run | [多吉云](https://www.dogecloud.com/) | 可部署到多吉云 CDN | | [BytePlus](https://www.byteplus.com/) | 可部署到 BytePlus CDN 等服务 | | [优刻得](https://www.ucloud.cn/) | 可部署到优刻得 US3、UCDN 等服务 | +| [Edgio](https://edg.io/) | 可部署到 Edgio Applications 等服务 | diff --git a/README_EN.md b/README_EN.md index 9c9aab55..ece9591c 100644 --- a/README_EN.md +++ b/README_EN.md @@ -126,6 +126,7 @@ The following hosting providers are supported: | [Doge Cloud](https://www.dogecloud.com/) | Supports deployment to Doge Cloud CDN | | [BytePlus](https://www.byteplus.com/) | Supports deployment to BytePlus CDN | | [UCloud](https://www.ucloud-global.com/) | Supports deployment to UCloud US3, UCDN | +| [Edgio](https://edg.io/) | Supports deployment to Edgio Applications | diff --git a/go.mod b/go.mod index 9d52261f..e6ba637a 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/baidubce/bce-sdk-go v0.9.209 github.com/byteplus-sdk/byteplus-sdk-golang v1.0.35 github.com/go-acme/lego/v4 v4.21.0 + github.com/go-resty/resty/v2 v2.16.3 github.com/go-viper/mapstructure/v2 v2.2.1 github.com/gojek/heimdall/v7 v7.0.3 github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128 diff --git a/go.sum b/go.sum index d41faffa..52df7389 100644 --- a/go.sum +++ b/go.sum @@ -398,6 +398,8 @@ github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa 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/validator/v10 v10.7.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk= +github.com/go-resty/resty/v2 v2.16.3 h1:zacNT7lt4b8M/io2Ahj6yPypL7bqx9n1iprfQuodV+E= +github.com/go-resty/resty/v2 v2.16.3/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= 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= diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go index 218b42fc..a89204c6 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -20,9 +20,9 @@ import ( ) type ApplyCertResult struct { - Certificate string - PrivateKey string + CertificateChain string IssuerCertificate string + PrivateKey string ACMECertUrl string ACMECertStableUrl string CSR string @@ -150,9 +150,9 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap } return &ApplyCertResult{ - PrivateKey: string(certResource.PrivateKey), - Certificate: string(certResource.Certificate), - IssuerCertificate: string(certResource.IssuerCertificate), + CertificateChain: strings.TrimSpace(string(certResource.Certificate)), + IssuerCertificate: strings.TrimSpace(string(certResource.IssuerCertificate)), + PrivateKey: strings.TrimSpace(string(certResource.PrivateKey)), ACMECertUrl: certResource.CertURL, ACMECertStableUrl: certResource.CertStableURL, CSR: string(certResource.CSR), diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 6cb192f9..6fd3457c 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -15,6 +15,7 @@ import ( providerBaiduCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-cdn" providerBytePlusCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/byteplus-cdn" providerDogeCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/dogecloud-cdn" + providerEdgioApplications "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/edgio-applications" providerHuaweiCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-cdn" providerHuaweiCloudELB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-elb" providerK8sSecret "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/k8s-secret" @@ -175,6 +176,21 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger, return deployer, logger, err } + case domain.DeployProviderTypeEdgioApplications: + { + access := domain.AccessConfigForEdgio{} + if err := maps.Decode(options.ProviderAccessConfig, &access); err != nil { + return nil, nil, fmt.Errorf("failed to decode provider access config: %w", err) + } + + deployer, err := providerEdgioApplications.NewWithLogger(&providerEdgioApplications.EdgioApplicationsDeployerConfig{ + ClientId: access.ClientId, + ClientSecret: access.ClientSecret, + EnvironmentId: maps.GetValueAsString(options.ProviderDeployConfig, "environmentId"), + }, logger) + return deployer, logger, err + } + case domain.DeployProviderTypeHuaweiCloudCDN, domain.DeployProviderTypeHuaweiCloudELB: { access := domain.AccessConfigForHuaweiCloud{} diff --git a/internal/domain/access.go b/internal/domain/access.go index dc118482..c9b1d2a9 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -66,6 +66,11 @@ type AccessConfigForDogeCloud struct { SecretKey string `json:"secretKey"` } +type AccessConfigForEdgio struct { + ClientId string `json:"clientId"` + ClientSecret string `json:"clientSecret"` +} + type AccessConfigForGoDaddy struct { ApiKey string `json:"apiKey"` ApiSecret string `json:"apiSecret"` diff --git a/internal/domain/provider.go b/internal/domain/provider.go index dead9c57..34666531 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -17,6 +17,7 @@ const ( AccessProviderTypeBytePlus = AccessProviderType("byteplus") AccessProviderTypeCloudflare = AccessProviderType("cloudflare") AccessProviderTypeDogeCloud = AccessProviderType("dogecloud") + AccessProviderTypeEdgio = AccessProviderType("edgio") AccessProviderTypeGoDaddy = AccessProviderType("godaddy") AccessProviderTypeHuaweiCloud = AccessProviderType("huaweicloud") AccessProviderTypeKubernetes = AccessProviderType("k8s") @@ -73,34 +74,35 @@ type DeployProviderType string NOTICE: If you add new constant, please keep ASCII order. */ const ( - DeployProviderTypeAliyunALB = DeployProviderType("aliyun-alb") - DeployProviderTypeAliyunCDN = DeployProviderType("aliyun-cdn") - DeployProviderTypeAliyunCLB = DeployProviderType("aliyun-clb") - DeployProviderTypeAliyunDCDN = DeployProviderType("aliyun-dcdn") - DeployProviderTypeAliyunLive = DeployProviderType("aliyun-live") - DeployProviderTypeAliyunNLB = DeployProviderType("aliyun-nlb") - DeployProviderTypeAliyunOSS = DeployProviderType("aliyun-oss") - DeployProviderTypeBaiduCloudCDN = DeployProviderType("baiducloud-cdn") - DeployProviderTypeBytePlusCDN = DeployProviderType("byteplus-cdn") - DeployProviderTypeDogeCloudCDN = DeployProviderType("dogecloud-cdn") - DeployProviderTypeHuaweiCloudCDN = DeployProviderType("huaweicloud-cdn") - DeployProviderTypeHuaweiCloudELB = DeployProviderType("huaweicloud-elb") - DeployProviderTypeKubernetesSecret = DeployProviderType("k8s-secret") - DeployProviderTypeLocal = DeployProviderType("local") - DeployProviderTypeQiniuCDN = DeployProviderType("qiniu-cdn") - DeployProviderTypeSSH = DeployProviderType("ssh") - DeployProviderTypeTencentCloudCDN = DeployProviderType("tencentcloud-cdn") - DeployProviderTypeTencentCloudCLB = DeployProviderType("tencentcloud-clb") - DeployProviderTypeTencentCloudCOS = DeployProviderType("tencentcloud-cos") - DeployProviderTypeTencentCloudCSS = DeployProviderType("tencentcloud-css") - DeployProviderTypeTencentCloudECDN = DeployProviderType("tencentcloud-ecdn") - DeployProviderTypeTencentCloudEO = DeployProviderType("tencentcloud-eo") - DeployProviderTypeUCloudUCDN = DeployProviderType("ucloud-ucdn") - DeployProviderTypeUCloudUS3 = DeployProviderType("ucloud-us3") - DeployProviderTypeVolcEngineCDN = DeployProviderType("volcengine-cdn") - DeployProviderTypeVolcEngineCLB = DeployProviderType("volcengine-clb") - DeployProviderTypeVolcEngineDCDN = DeployProviderType("volcengine-dcdn") - DeployProviderTypeVolcEngineLive = DeployProviderType("volcengine-live") - DeployProviderTypeVolcEngineTOS = DeployProviderType("volcengine-tos") - DeployProviderTypeWebhook = DeployProviderType("webhook") + DeployProviderTypeAliyunALB = DeployProviderType("aliyun-alb") + DeployProviderTypeAliyunCDN = DeployProviderType("aliyun-cdn") + DeployProviderTypeAliyunCLB = DeployProviderType("aliyun-clb") + DeployProviderTypeAliyunDCDN = DeployProviderType("aliyun-dcdn") + DeployProviderTypeAliyunLive = DeployProviderType("aliyun-live") + DeployProviderTypeAliyunNLB = DeployProviderType("aliyun-nlb") + DeployProviderTypeAliyunOSS = DeployProviderType("aliyun-oss") + DeployProviderTypeBaiduCloudCDN = DeployProviderType("baiducloud-cdn") + DeployProviderTypeBytePlusCDN = DeployProviderType("byteplus-cdn") + DeployProviderTypeDogeCloudCDN = DeployProviderType("dogecloud-cdn") + DeployProviderTypeEdgioApplications = DeployProviderType("edgio-applications") + DeployProviderTypeHuaweiCloudCDN = DeployProviderType("huaweicloud-cdn") + DeployProviderTypeHuaweiCloudELB = DeployProviderType("huaweicloud-elb") + DeployProviderTypeKubernetesSecret = DeployProviderType("k8s-secret") + DeployProviderTypeLocal = DeployProviderType("local") + DeployProviderTypeQiniuCDN = DeployProviderType("qiniu-cdn") + DeployProviderTypeSSH = DeployProviderType("ssh") + DeployProviderTypeTencentCloudCDN = DeployProviderType("tencentcloud-cdn") + DeployProviderTypeTencentCloudCLB = DeployProviderType("tencentcloud-clb") + DeployProviderTypeTencentCloudCOS = DeployProviderType("tencentcloud-cos") + DeployProviderTypeTencentCloudCSS = DeployProviderType("tencentcloud-css") + DeployProviderTypeTencentCloudECDN = DeployProviderType("tencentcloud-ecdn") + DeployProviderTypeTencentCloudEO = DeployProviderType("tencentcloud-eo") + DeployProviderTypeUCloudUCDN = DeployProviderType("ucloud-ucdn") + DeployProviderTypeUCloudUS3 = DeployProviderType("ucloud-us3") + DeployProviderTypeVolcEngineCDN = DeployProviderType("volcengine-cdn") + DeployProviderTypeVolcEngineCLB = DeployProviderType("volcengine-clb") + DeployProviderTypeVolcEngineDCDN = DeployProviderType("volcengine-dcdn") + DeployProviderTypeVolcEngineLive = DeployProviderType("volcengine-live") + DeployProviderTypeVolcEngineTOS = DeployProviderType("volcengine-tos") + DeployProviderTypeWebhook = DeployProviderType("webhook") ) diff --git a/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go b/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go new file mode 100644 index 00000000..d4d70e04 --- /dev/null +++ b/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go @@ -0,0 +1,112 @@ +package edgioapplications + +import ( + "context" + "encoding/pem" + "errors" + + xerrors "github.com/pkg/errors" + + "github.com/usual2970/certimate/internal/pkg/core/deployer" + "github.com/usual2970/certimate/internal/pkg/core/logger" + edgsdk "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7" + edgsdkDtos "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7/dtos" +) + +type EdgioApplicationsDeployerConfig struct { + // Edgio ClientId。 + ClientId string `json:"clientId"` + // Edgio ClientSecret。 + ClientSecret string `json:"clientSecret"` + // Edgio 环境 ID。 + EnvironmentId string `json:"environmentId"` +} + +type EdgioApplicationsDeployer struct { + config *EdgioApplicationsDeployerConfig + logger logger.Logger + sdkClient *edgsdk.EdgioClient +} + +var _ deployer.Deployer = (*EdgioApplicationsDeployer)(nil) + +func New(config *EdgioApplicationsDeployerConfig) (*EdgioApplicationsDeployer, error) { + return NewWithLogger(config, logger.NewNilLogger()) +} + +func NewWithLogger(config *EdgioApplicationsDeployerConfig, logger logger.Logger) (*EdgioApplicationsDeployer, error) { + if config == nil { + return nil, errors.New("config is nil") + } + + if logger == nil { + return nil, errors.New("logger is nil") + } + + client, err := createSdkClient(config.ClientId, config.ClientSecret) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk client") + } + + return &EdgioApplicationsDeployer{ + logger: logger, + config: config, + sdkClient: client, + }, nil +} + +func (d *EdgioApplicationsDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) { + // 提取 Edgio 所需的服务端证书和中间证书内容 + privateCertPem, intermediateCertPem := extractCertChains(certPem) + + // 上传 TLS 证书 + // REF: https://docs.edg.io/rest_api/#tag/tls-certs/operation/postConfigV01TlsCerts + uploadTlsCertReq := edgsdkDtos.UploadTlsCertRequest{ + EnvironmentID: d.config.EnvironmentId, + PrimaryCert: privateCertPem, + IntermediateCert: intermediateCertPem, + PrivateKey: privkeyPem, + } + uploadTlsCertResp, err := d.sdkClient.UploadTlsCert(uploadTlsCertReq) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'edgio.UploadTlsCert'") + } + + d.logger.Logt("已上传 TLS 证书", uploadTlsCertResp) + + return &deployer.DeployResult{}, nil +} + +func createSdkClient(clientId, clientSecret string) (*edgsdk.EdgioClient, error) { + client := edgsdk.NewEdgioClient(clientId, clientSecret, "", "") + return client, nil +} + +func extractCertChains(certPem string) (primaryCertPem string, intermediateCertPem string) { + pemBlocks := make([]*pem.Block, 0) + pemData := []byte(certPem) + for { + block, rest := pem.Decode(pemData) + if block == nil { + break + } + + pemBlocks = append(pemBlocks, block) + pemData = rest + } + + primaryCertPem = "" + intermediateCertPem = "" + + if len(pemBlocks) > 0 { + primaryCertPem = string(pem.EncodeToMemory(pemBlocks[0])) + } + + if len(pemBlocks) > 1 { + for i := 1; i < len(pemBlocks); i++ { + intermediateCertPem += string(pem.EncodeToMemory(pemBlocks[i])) + } + } + + return primaryCertPem, intermediateCertPem +} diff --git a/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications_test.go b/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications_test.go new file mode 100644 index 00000000..04f7a4cb --- /dev/null +++ b/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications_test.go @@ -0,0 +1,75 @@ +package edgioapplications_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/edgio-applications" +) + +var ( + fInputCertPath string + fInputKeyPath string + fClientId string + fClientSecret string + fEnvironmentId string +) + +func init() { + argsPrefix := "CERTIMATE_DEPLOYER_EDGIOAPPLICATIONS_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fClientId, argsPrefix+"CLIENTID", "", "") + flag.StringVar(&fClientSecret, argsPrefix+"CLIENTSECRET", "", "") + flag.StringVar(&fEnvironmentId, argsPrefix+"ENVIRONMENTID", "", "") +} + +/* +Shell command to run this test: + + go test -v ./edgio_applications_test.go -args \ + --CERTIMATE_DEPLOYER_EDGIOAPPLICATIONS_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_DEPLOYER_EDGIOAPPLICATIONS_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_EDGIOAPPLICATIONS_CLIENTID="your-client-id" \ + --CERTIMATE_DEPLOYER_EDGIOAPPLICATIONS_CLIENTSECRET="your-client-secret" \ + --CERTIMATE_DEPLOYER_EDGIOAPPLICATIONS_ENVIRONMENTID="your-enviroment-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("CLIENTID: %v", fClientId), + fmt.Sprintf("CLIENTSECRET: %v", fClientSecret), + fmt.Sprintf("ENVIRONMENTID: %v", fEnvironmentId), + }, "\n")) + + deployer, err := provider.New(&provider.EdgioApplicationsDeployerConfig{ + ClientId: fClientId, + ClientSecret: fClientSecret, + EnvironmentId: fEnvironmentId, + }) + 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) + }) +} diff --git a/internal/pkg/vendors/edgio-sdk/applications/v7/README.md b/internal/pkg/vendors/edgio-sdk/applications/v7/README.md new file mode 100644 index 00000000..fae60236 --- /dev/null +++ b/internal/pkg/vendors/edgio-sdk/applications/v7/README.md @@ -0,0 +1,3 @@ +```shell +git clone https://github.com/Edgio/terraform-provider-edgio.git +``` diff --git a/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/cdn_configuration.go b/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/cdn_configuration.go new file mode 100644 index 00000000..2ae5d434 --- /dev/null +++ b/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/cdn_configuration.go @@ -0,0 +1,93 @@ +package dtos + +import "encoding/json" + +type CDNConfiguration struct { + ConfigurationID string `json:"id"` + EnvironmentID string `json:"environment_id"` + Rules json.RawMessage `json:"rules"` + Origins []Origin `json:"origins"` + Hostnames []Hostname `json:"hostnames"` + Experiments *[]string `json:"experiments,omitempty"` + EdgeFunctionsSources *map[string]string `json:"edge_functions_sources,omitempty"` + EdgeFunctionInitScript *string `json:"edge_function_init_script,omitempty"` +} + +type Origin struct { + Name string `json:"name"` + Type *string `json:"type,omitempty"` + Hosts []Host `json:"hosts"` + Balancer *string `json:"balancer,omitempty"` + OverrideHostHeader *string `json:"override_host_header,omitempty"` + Shields *Shields `json:"shields,omitempty"` + PciCertifiedShields *bool `json:"pci_certified_shields,omitempty"` + TLSVerify *TLSVerify `json:"tls_verify,omitempty"` + Retry *Retry `json:"retry,omitempty"` +} + +type Host struct { + Weight *int64 `json:"weight,omitempty"` + DNSMaxTTL *int64 `json:"dns_max_ttl,omitempty"` + DNSPreference *string `json:"dns_preference,omitempty"` + MaxHardPool *int64 `json:"max_hard_pool,omitempty"` + DNSMinTTL *int64 `json:"dns_min_ttl,omitempty"` + Location *[]Location `json:"location,omitempty"` + MaxPool *int64 `json:"max_pool,omitempty"` + Balancer *string `json:"balancer,omitempty"` + Scheme *string `json:"scheme,omitempty"` + OverrideHostHeader *string `json:"override_host_header,omitempty"` + SNIHintAndStrictSanCheck *string `json:"sni_hint_and_strict_san_check,omitempty"` + UseSNI *bool `json:"use_sni,omitempty"` +} + +type Location struct { + Port *int64 `json:"port,omitempty"` + Hostname *string `json:"hostname,omitempty"` +} + +type Shields struct { + Apac *string `json:"apac,omitempty"` + Emea *string `json:"emea,omitempty"` + USWest *string `json:"us_west,omitempty"` + USEast *string `json:"us_east,omitempty"` +} + +type TLSVerify struct { + UseSNI *bool `json:"use_sni,omitempty"` + SNIHintAndStrictSanCheck *string `json:"sni_hint_and_strict_san_check,omitempty"` + AllowSelfSignedCerts *bool `json:"allow_self_signed_certs,omitempty"` + PinnedCerts *[]string `json:"pinned_certs,omitempty"` +} + +type Retry struct { + StatusCodes *[]int64 `json:"status_codes,omitempty"` + IgnoreRetryAfterHeader *bool `json:"ignore_retry_after_header,omitempty"` + AfterSeconds *int64 `json:"after_seconds,omitempty"` + MaxRequests *int64 `json:"max_requests,omitempty"` + MaxWaitSeconds *int64 `json:"max_wait_seconds,omitempty"` +} + +type Hostname struct { + Hostname *string `json:"hostname,omitempty"` + DefaultOriginName *string `json:"default_origin_name,omitempty"` + ReportCode *int64 `json:"report_code,omitempty"` + TLS *TLS `json:"tls,omitempty"` + Directory *string `json:"directory,omitempty"` +} + +type TLS struct { + NPN *bool `json:"npn,omitempty"` + ALPN *bool `json:"alpn,omitempty"` + Protocols *string `json:"protocols,omitempty"` + UseSigAlgs *bool `json:"use_sigalgs,omitempty"` + SNI *bool `json:"sni,omitempty"` + SniStrict *bool `json:"sni_strict,omitempty"` + SniHostMatch *bool `json:"sni_host_match,omitempty"` + ClientRenegotiation *bool `json:"client_renegotiation,omitempty"` + Options *string `json:"options,omitempty"` + CipherList *string `json:"cipher_list,omitempty"` + NamedCurve *string `json:"named_curve,omitempty"` + OCSP *bool `json:"oscp,omitempty"` + PEM *string `json:"pem,omitempty"` + CA *string `json:"ca,omitempty"` +} diff --git a/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/environment.go b/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/environment.go new file mode 100644 index 00000000..74d99dd6 --- /dev/null +++ b/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/environment.go @@ -0,0 +1,29 @@ +package dtos + +import ( + "time" +) + +type Environment struct { + Type string `json:"@type"` + IdLink string `json:"@id"` + Id string `json:"id"` + PropertyID string `json:"property_id"` + LegacyAccountNumber string `json:"legacy_account_number"` + Name string `json:"name"` + CanMembersDeploy bool `json:"can_members_deploy"` + OnlyMaintainersCanDeploy bool `json:"only_maintainers_can_deploy"` + HttpRequestLogging bool `json:"http_request_logging"` + DefaultDomainName string `json:"default_domain_name"` + PciCompliance bool `json:"pci_compliance"` + DnsDomainName string `json:"dns_domain_name"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type EnvironmentsResponse struct { + Type string `json:"@type"` + Id string `json:"@id"` + TotalItems int `json:"total_items"` + Items []Environment `json:"items"` +} diff --git a/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/property.go b/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/property.go new file mode 100644 index 00000000..378ab999 --- /dev/null +++ b/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/property.go @@ -0,0 +1,18 @@ +package dtos + +import "time" + +type Property struct { + IdLink string `json:"@id"` + Id string `json:"id"` + OrganizationID string `json:"organization_id"` + Slug string `json:"slug"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type Properties struct { + ID string `json:"@id"` + TotalItems int `json:"total_items"` + Items []Property `json:"items"` +} diff --git a/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/purge.go b/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/purge.go new file mode 100644 index 00000000..67ad2612 --- /dev/null +++ b/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/purge.go @@ -0,0 +1,18 @@ +package dtos + +import "time" + +type PurgeResponse struct { + ID string `json:"id"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` + CompletedAt time.Time `json:"completed_at"` + ProgressPercentage float32 `json:"progress_percentage"` +} + +type PurgeRequest struct { + EnvironmentID string `json:"environment_id"` + PurgeType string `json:"purge_type"` + Values []string `json:"values"` + Hostname *string `json:"hostname"` +} diff --git a/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/tls_cert.go b/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/tls_cert.go new file mode 100644 index 00000000..08da9b4c --- /dev/null +++ b/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/tls_cert.go @@ -0,0 +1,30 @@ +package dtos + +type TLSCertResponse struct { + ID string `json:"id"` + EnvironmentID string `json:"environment_id"` + PrimaryCert string `json:"primary_cert"` + IntermediateCert string `json:"intermediate_cert"` + Expiration string `json:"expiration"` + Status string `json:"status"` + Generated bool `json:"generated"` + Serial string `json:"serial"` + CommonName string `json:"common_name"` + AlternativeNames []string `json:"alternative_names"` + ActivationError string `json:"activation_error"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +type UploadTlsCertRequest struct { + EnvironmentID string `json:"environment_id"` + PrimaryCert string `json:"primary_cert"` + IntermediateCert string `json:"intermediate_cert"` + PrivateKey string `json:"private_key"` +} + +type TLSCertSResponse struct { + EnvironmentID string `json:"environment_id"` + TotalItems int32 `json:"total_items"` + Certificates []TLSCertResponse `json:"items"` +} diff --git a/internal/pkg/vendors/edgio-sdk/applications/v7/edgio_client.go b/internal/pkg/vendors/edgio-sdk/applications/v7/edgio_client.go new file mode 100644 index 00000000..a03436fc --- /dev/null +++ b/internal/pkg/vendors/edgio-sdk/applications/v7/edgio_client.go @@ -0,0 +1,546 @@ +package edgio_api + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/go-resty/resty/v2" + + "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7/dtos" +) + +// AccessTokenResponse represents the response from the token endpoint. +type AccessTokenResponse struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + TokenType string `json:"token_type"` + Scope string `json:"scope"` +} + +// TokenCache represents a cached token. The token is stored along +// with its expiry time. Because different endpoints require different +// scopes, we store the token with the scope as the key, so that we +// can fetch the token from the cache based on the scope. +type TokenCache struct { + AccessToken string + Expiry time.Time +} + +type EdgioClient struct { + client *resty.Client + clientID string + clientSecret string + tokenURL string + apiURL string + tokenCache map[string]TokenCache +} + +func NewEdgioClient(clientID, clientSecret, tokenURL, apiURL string) *EdgioClient { + client := resty.New(). + SetTimeout(30 * time.Second). + SetRetryCount(3). + SetRetryWaitTime(5 * time.Second). + SetRetryMaxWaitTime(20 * time.Second) + + if tokenURL == "" { + tokenURL = "https://id.edgio.app/connect/token" + } + + if apiURL == "" { + apiURL = "https://edgioapis.com" + } + + return &EdgioClient{ + client: client, + clientID: clientID, + clientSecret: clientSecret, + tokenURL: tokenURL, + apiURL: apiURL, + tokenCache: make(map[string]TokenCache), + } +} + +func (c *EdgioClient) getToken(scope string) (string, error) { + if cachedToken, exists := c.tokenCache[scope]; exists && time.Now().Before(cachedToken.Expiry) { + return cachedToken.AccessToken, nil + } + + var tokenResp AccessTokenResponse + resp, err := c.client.R(). + SetFormData(map[string]string{ + "client_id": c.clientID, + "client_secret": c.clientSecret, + "grant_type": "client_credentials", + "scope": scope, + }). + SetResult(&tokenResp). + Post(c.tokenURL) + if err != nil { + return "", fmt.Errorf("failed to request token: %w", err) + } + + if resp.IsError() { + return "", fmt.Errorf("unexpected status code for getToken: %d", resp.StatusCode()) + } + + c.tokenCache[scope] = TokenCache{ + AccessToken: tokenResp.AccessToken, + Expiry: time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second), + } + + return tokenResp.AccessToken, nil +} + +func (c *EdgioClient) GetProperty(ctx context.Context, propertyID string) (*dtos.Property, error) { + token, err := c.getToken("app.accounts") + if err != nil { + return nil, fmt.Errorf("failed to get token: %w", err) + } + + url := fmt.Sprintf("%s/accounts/v0.1/properties/%s", c.apiURL, propertyID) + + var property dtos.Property + resp, err := c.client.R(). + SetContext(ctx). + SetAuthToken(token). + SetResult(&property). + Get(url) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + + if resp.IsError() { + return nil, fmt.Errorf("unexpected status code for getSpecificProperty: %d, %s", resp.StatusCode(), resp.Request.URL) + } + + return &property, nil +} + +func (c *EdgioClient) GetProperties(page int, pageSize int, organizationID string) (*dtos.Properties, error) { + token, err := c.getToken("app.accounts") + if err != nil { + return nil, fmt.Errorf("failed to get token: %w", err) + } + + url := fmt.Sprintf("%s/accounts/v0.1/properties", c.apiURL) + + var propertiesResp dtos.Properties + resp, err := c.client.R(). + SetAuthToken(token). + SetQueryParams(map[string]string{ + "page": fmt.Sprintf("%d", page), + "page_size": fmt.Sprintf("%d", pageSize), + "organization_id": organizationID, + }). + SetResult(&propertiesResp). + Get(url) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + + if resp.IsError() { + return nil, fmt.Errorf("unexpected status code for getProperties: %d, %s", resp.StatusCode(), resp.Body()) + } + + return &propertiesResp, nil +} + +func (c *EdgioClient) CreateProperty(ctx context.Context, organizationID, slug string) (*dtos.Property, error) { + token, err := c.getToken("app.accounts") + if err != nil { + return nil, fmt.Errorf("failed to get token: %w", err) + } + + url := fmt.Sprintf("%s/accounts/v0.1/properties", c.apiURL) + + var createdProperty dtos.Property + resp, err := c.client.R(). + SetContext(ctx). + SetAuthToken(token). + SetHeader("Content-Type", "application/json"). + SetBody(map[string]string{ + "organization_id": organizationID, + "slug": slug, + }). + SetResult(&createdProperty). + Post(url) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + + if resp.IsError() { + return nil, fmt.Errorf("unexpected status code for createProperty: %d, response: %s", resp.StatusCode(), resp.String()) + } + + return &createdProperty, nil +} + +func (c *EdgioClient) DeleteProperty(propertyID string) error { + token, err := c.getToken("app.accounts") + if err != nil { + return fmt.Errorf("failed to get token: %w", err) + } + + url := fmt.Sprintf("%s/accounts/v0.1/properties/%s", c.apiURL, propertyID) + + resp, err := c.client.R(). + SetAuthToken(token). + Delete(url) + if err != nil { + return fmt.Errorf("error sending DELETE request: %w", err) + } + + if resp.IsError() { + return fmt.Errorf("error deleting property: status code %d", resp.StatusCode()) + } + + return nil +} + +func (c *EdgioClient) UpdateProperty(ctx context.Context, propertyID string, slug string) (*dtos.Property, error) { + token, err := c.getToken("app.accounts") + if err != nil { + return nil, fmt.Errorf("failed to get token: %w", err) + } + + url := fmt.Sprintf("%s/accounts/v0.1/properties/%s", c.apiURL, propertyID) + + requestBody := map[string]interface{}{ + "slug": slug, + } + + var updatedProperty dtos.Property + resp, err := c.client.R(). + SetContext(ctx). + SetAuthToken(token). + SetBody(requestBody). + SetResult(&updatedProperty). + Patch(url) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + + if resp.IsError() { + return nil, fmt.Errorf("unexpected status code for updateProperty: %d", resp.StatusCode()) + } + + return &updatedProperty, nil +} + +func (c *EdgioClient) GetEnvironments(page, pageSize int, propertyID string) (*dtos.EnvironmentsResponse, error) { + token, err := c.getToken("app.accounts") + if err != nil { + return nil, fmt.Errorf("failed to get token: %w", err) + } + + url := fmt.Sprintf("%s/accounts/v0.1/environments", c.apiURL) + + resp, err := c.client.R(). + SetAuthToken(token). + SetQueryParams(map[string]string{ + "page": fmt.Sprintf("%d", page), + "page_size": fmt.Sprintf("%d", pageSize), + "property_id": propertyID, + }). + SetResult(&dtos.EnvironmentsResponse{}). + Get(url) + if err != nil { + return nil, err + } + + if resp.IsError() { + return nil, fmt.Errorf("error response: %s", resp.String()) + } + + return resp.Result().(*dtos.EnvironmentsResponse), nil +} + +func (c *EdgioClient) GetEnvironment(environmentID string) (*dtos.Environment, error) { + token, err := c.getToken("app.accounts") + if err != nil { + return nil, fmt.Errorf("failed to get token: %w", err) + } + + url := fmt.Sprintf("%s/accounts/v0.1/environments/%s", c.apiURL, environmentID) + + resp, err := c.client.R(). + SetPathParams(map[string]string{ + "environment_id": environmentID, + }). + SetAuthToken(token). + SetResult(&dtos.Environment{}). + Get(url) + if err != nil { + return nil, err + } + + if resp.IsError() { + return nil, fmt.Errorf("error response: %s", resp.String()) + } + + return resp.Result().(*dtos.Environment), nil +} + +func (c *EdgioClient) CreateEnvironment(propertyID, name string, onlyMaintainersCanDeploy, httpRequestLogging bool) (*dtos.Environment, error) { + token, err := c.getToken("app.accounts") + if err != nil { + return nil, fmt.Errorf("failed to get token: %w", err) + } + + url := fmt.Sprintf("%s/accounts/v0.1/environments", c.apiURL) + + body := map[string]interface{}{ + "property_id": propertyID, + "name": name, + "only_maintainers_can_deploy": onlyMaintainersCanDeploy, + "http_request_logging": httpRequestLogging, + } + + resp, err := c.client.R(). + SetBody(body). + SetAuthToken(token). + SetResult(&dtos.Environment{}). + Post(url) + if err != nil { + return nil, err + } + + if resp.IsError() { + return nil, fmt.Errorf("error response: %s", resp.String()) + } + + return resp.Result().(*dtos.Environment), nil +} + +func (c *EdgioClient) UpdateEnvironment(environmentID, name string, onlyMaintainersCanDeploy, httpRequestLogging, preserveCache bool) (*dtos.Environment, error) { + token, err := c.getToken("app.accounts") + if err != nil { + return nil, fmt.Errorf("failed to get token: %w", err) + } + + url := fmt.Sprintf("%s/accounts/v0.1/environments/%s", c.apiURL, environmentID) + + body := map[string]interface{}{ + "name": name, + // as can_members_deploy is depricated, but update api is not + // we need to use it to map onlyMaintainersCanDeploy + "only_maintainers_can_deploy": onlyMaintainersCanDeploy, + "http_request_logging": httpRequestLogging, + "preserve_cache": preserveCache, + } + + resp, err := c.client.R(). + SetPathParams(map[string]string{ + "environment_id": environmentID, + }). + SetBody(body). + SetAuthToken(token). + SetResult(&dtos.Environment{}). + Patch(url) + if err != nil { + return nil, err + } + + if resp.IsError() { + return nil, fmt.Errorf("error response: %s", resp.String()) + } + + return resp.Result().(*dtos.Environment), nil +} + +func (c *EdgioClient) DeleteEnvironment(environmentID string) error { + token, err := c.getToken("app.accounts") + if err != nil { + return fmt.Errorf("failed to get token: %w", err) + } + + url := fmt.Sprintf("%s/accounts/v0.1/environments/%s", c.apiURL, environmentID) + + resp, err := c.client.R(). + SetPathParams(map[string]string{ + "environment_id": environmentID, + }). + SetAuthToken(token). + SetResult(&dtos.Environment{}). + Delete(url) + if err != nil { + return err + } + + if resp.IsError() { + return fmt.Errorf("error response: %s", resp.String()) + } + + return nil +} + +func (c *EdgioClient) GetTlsCert(tlsCertId string) (*dtos.TLSCertResponse, error) { + token, err := c.getToken("app.config") + if err != nil { + return nil, fmt.Errorf("failed to get token: %w", err) + } + + url := fmt.Sprintf("%s/config/v0.1/tls-certs/%s", c.apiURL, tlsCertId) + + var tlsCertResponse dtos.TLSCertResponse + resp, err := c.client.R(). + SetAuthToken(token). + SetResult(&tlsCertResponse). + Get(url) + if err != nil { + return nil, fmt.Errorf("error response: %s", err) + } + + if resp.IsError() { + return nil, fmt.Errorf("error response: %s", resp.String()) + } + + return &tlsCertResponse, nil +} + +func (c *EdgioClient) UploadTlsCert(req dtos.UploadTlsCertRequest) (*dtos.TLSCertResponse, error) { + token, err := c.getToken("app.config") + if err != nil { + return nil, fmt.Errorf("failed to get token: %w", err) + } + + url := fmt.Sprintf("%s/config/v0.1/tls-certs", c.apiURL) + response := &dtos.TLSCertResponse{} + + resp, err := c.client.R(). + SetAuthToken(token). + SetHeader("Content-Type", "application/json"). + SetBody(req). + SetResult(response). + Post(url) + if err != nil { + return nil, fmt.Errorf("failed to upload TLS certificate: %w", err) + } + + if resp.IsError() { + return nil, fmt.Errorf("API responded with error: %s", resp.String()) + } + + return response, nil +} + +func (c *EdgioClient) GenerateTlsCert(environmentId string) (*dtos.TLSCertResponse, error) { + token, err := c.getToken("app.config") + if err != nil { + return nil, fmt.Errorf("failed to get token: %w", err) + } + + url := fmt.Sprintf("%s/config/v0.1/tls-certs/generate", c.apiURL) + request := map[string]interface{}{ + "environment_id": environmentId, + } + response := &dtos.TLSCertResponse{} + + resp, err := c.client.R(). + SetAuthToken(token). + SetHeader("Content-Type", "application/json"). + SetBody(request). + SetResult(response). + Post(url) + if err != nil { + return nil, fmt.Errorf("failed to upload TLS certificate: %w", err) + } + + if resp.IsError() { + return nil, fmt.Errorf("API responded with error: %s", resp.String()) + } + + return response, nil +} + +func (c *EdgioClient) GetTlsCerts(page int, pageSize int, environmentID string) (*dtos.TLSCertSResponse, error) { + token, err := c.getToken("app.config") + if err != nil { + return nil, fmt.Errorf("failed to get token: %w", err) + } + + url := fmt.Sprintf("%s/config/v0.1/tls-certs", c.apiURL) + + var tlsCertsResponse dtos.TLSCertSResponse + resp, err := c.client.R(). + SetAuthToken(token). + SetQueryParams(map[string]string{ + "page": fmt.Sprintf("%d", page), + "page_size": fmt.Sprintf("%d", pageSize), + "environment_id": environmentID, + }). + SetResult(&tlsCertsResponse). + Get(url) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + + if resp.IsError() { + return nil, fmt.Errorf("unexpected status code for getTlsCerts: %d", resp.StatusCode()) + } + + return &tlsCertsResponse, nil +} + +func (c *EdgioClient) UploadCdnConfiguration(config *dtos.CDNConfiguration) (*dtos.CDNConfiguration, error) { + fmt.Println("------------------------------------------------------------------------- uploading") + + token, err := c.getToken("app.config") + if err != nil { + return nil, fmt.Errorf("failed to get token: %w", err) + } + + url := fmt.Sprintf("%s/config/v0.1/configs", c.apiURL) + var response dtos.CDNConfiguration + + // Convert config to json + jsonBody, _ := json.MarshalIndent(config, "", " ") + jsonString := string(jsonBody) + fmt.Println("------------------------- config report code: ", config.Hostnames[0].ReportCode == nil) + fmt.Println("------------------------- config report code value: ", config.Hostnames[0].ReportCode) + fmt.Println("----------------------------------- jsonBody: ", jsonString) + + resp, err := c.client.R(). + SetAuthToken(token). + SetHeader("Content-Type", "application/json"). + SetBody(config). + SetResult(&response). + Post(url) + if err != nil { + return nil, fmt.Errorf("failed to upload CDN configuration: %w", err) + } + + if resp.IsError() { + return nil, fmt.Errorf("unexpected status code for uploadCdnConfiguration: %d, %s", resp.StatusCode(), resp.Body()) + } + + return &response, nil +} + +func (c *EdgioClient) GetCDNConfiguration(configID string) (*dtos.CDNConfiguration, error) { + fmt.Println("------------------------------------------------------------------------- reading config") + + token, err := c.getToken("app.config") + if err != nil { + return nil, fmt.Errorf("failed to get token: %w", err) + } + + url := fmt.Sprintf("https://edgioapis.com/config/v0.1/configs/%s", configID) + var response dtos.CDNConfiguration + + resp, err := c.client.R(). + SetAuthToken(token). + SetResult(&response). + Get(url) + if err != nil { + return nil, fmt.Errorf("failed to get CDN configuration: %w", err) + } + + if resp.IsError() { + return nil, fmt.Errorf("unexpected status code for GetCDNConfiguration: %d", resp.StatusCode()) + } + + return &response, nil +} diff --git a/internal/pkg/vendors/edgio-sdk/applications/v7/edgio_client_interface.go b/internal/pkg/vendors/edgio-sdk/applications/v7/edgio_client_interface.go new file mode 100644 index 00000000..ea5fa958 --- /dev/null +++ b/internal/pkg/vendors/edgio-sdk/applications/v7/edgio_client_interface.go @@ -0,0 +1,26 @@ +package edgio_api + +import ( + "context" + + "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7/dtos" +) + +type EdgioClientInterface interface { + GetProperty(ctx context.Context, propertyID string) (*dtos.Property, error) + GetProperties(page int, pageSize int, organizationID string) (*dtos.Properties, error) + CreateProperty(ctx context.Context, organizationID, slug string) (*dtos.Property, error) + DeleteProperty(propertyID string) error + UpdateProperty(ctx context.Context, propertyID string, slug string) (*dtos.Property, error) + GetEnvironments(page, pageSize int, propertyID string) (*dtos.EnvironmentsResponse, error) + GetEnvironment(environmentID string) (*dtos.Environment, error) + CreateEnvironment(propertyID, name string, onlyMaintainersCanDeploy, httpRequestLogging bool) (*dtos.Environment, error) + UpdateEnvironment(environmentID, name string, onlyMaintainersCanDeploy, httpRequestLogging, preserveCache bool) (*dtos.Environment, error) + DeleteEnvironment(environmentID string) error + GetTlsCert(tlsCertId string) (*dtos.TLSCertResponse, error) + UploadTlsCert(req dtos.UploadTlsCertRequest) (*dtos.TLSCertResponse, error) + GenerateTlsCert(environmentId string) (*dtos.TLSCertResponse, error) + GetTlsCerts(page int, pageSize int, environmentID string) (*dtos.TLSCertSResponse, error) + UploadCdnConfiguration(config *dtos.CDNConfiguration) (*dtos.CDNConfiguration, error) + GetCDNConfiguration(configID string) (*dtos.CDNConfiguration, error) +} diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index d214ca63..7ed5861a 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -89,7 +89,7 @@ func (a *applyNode) Run(ctx context.Context) error { Outputs: a.node.Outputs, } - certX509, err := x509.ParseCertificateFromPEM(applyResult.Certificate) + certX509, err := x509.ParseCertificateFromPEM(applyResult.CertificateChain) if err != nil { a.AddOutput(ctx, a.node.Name, "解析证书失败", err.Error()) return err @@ -98,7 +98,7 @@ func (a *applyNode) Run(ctx context.Context) error { certificate := &domain.Certificate{ Source: domain.CertificateSourceTypeWorkflow, SubjectAltNames: strings.Join(certX509.DNSNames, ";"), - Certificate: applyResult.Certificate, + Certificate: applyResult.CertificateChain, PrivateKey: applyResult.PrivateKey, IssuerCertificate: applyResult.IssuerCertificate, ACMECertUrl: applyResult.ACMECertUrl, diff --git a/ui/public/imgs/providers/edgio.svg b/ui/public/imgs/providers/edgio.svg new file mode 100644 index 00000000..72f21b47 --- /dev/null +++ b/ui/public/imgs/providers/edgio.svg @@ -0,0 +1 @@ + diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx index 040e52dd..afe49ccd 100644 --- a/ui/src/components/access/AccessForm.tsx +++ b/ui/src/components/access/AccessForm.tsx @@ -17,6 +17,7 @@ import AccessFormBaiduCloudConfig from "./AccessFormBaiduCloudConfig"; import AccessFormBytePlusConfig from "./AccessFormBytePlusConfig"; import AccessFormCloudflareConfig from "./AccessFormCloudflareConfig"; import AccessFormDogeCloudConfig from "./AccessFormDogeCloudConfig"; +import AccessFormEdgioConfig from "./AccessFormEdgioConfig"; import AccessFormGoDaddyConfig from "./AccessFormGoDaddyConfig"; import AccessFormHuaweiCloudConfig from "./AccessFormHuaweiCloudConfig"; import AccessFormKubernetesConfig from "./AccessFormKubernetesConfig"; @@ -102,6 +103,8 @@ const AccessForm = forwardRef(({ className, return ; case ACCESS_PROVIDERS.GODADDY: return ; + case ACCESS_PROVIDERS.EDGIO: + return ; case ACCESS_PROVIDERS.HUAWEICLOUD: return ; case ACCESS_PROVIDERS.KUBERNETES: diff --git a/ui/src/components/access/AccessFormEdgioConfig.tsx b/ui/src/components/access/AccessFormEdgioConfig.tsx new file mode 100644 index 00000000..d70ece6e --- /dev/null +++ b/ui/src/components/access/AccessFormEdgioConfig.tsx @@ -0,0 +1,76 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type AccessConfigForEdgio } from "@/domain/access"; + +type AccessFormEdgioConfigFieldValues = Nullish; + +export type AccessFormEdgioConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: AccessFormEdgioConfigFieldValues; + onValuesChange?: (values: AccessFormEdgioConfigFieldValues) => void; +}; + +const initFormModel = (): AccessFormEdgioConfigFieldValues => { + return { + clientId: "", + clientSecret: "", + }; +}; + +const AccessFormEdgioConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormEdgioConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + clientId: z + .string() + .min(1, t("access.form.edgio_client_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })) + .trim(), + clientSecret: z + .string() + .min(1, t("access.form.edgio_client_secret.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })) + .trim(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessFormEdgioConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index cbeb6c7a..77516024 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -25,6 +25,7 @@ import DeployNodeConfigFormAliyunOSSConfig from "./DeployNodeConfigFormAliyunOSS import DeployNodeConfigFormBaiduCloudCDNConfig from "./DeployNodeConfigFormBaiduCloudCDNConfig"; import DeployNodeConfigFormBytePlusCDNConfig from "./DeployNodeConfigFormBytePlusCDNConfig"; import DeployNodeConfigFormDogeCloudCDNConfig from "./DeployNodeConfigFormDogeCloudCDNConfig"; +import DeployNodeConfigFormEdgioApplicationsConfig from "./DeployNodeConfigFormEdgioApplicationsConfig"; import DeployNodeConfigFormHuaweiCloudCDNConfig from "./DeployNodeConfigFormHuaweiCloudCDNConfig"; import DeployNodeConfigFormHuaweiCloudELBConfig from "./DeployNodeConfigFormHuaweiCloudELBConfig"; import DeployNodeConfigFormKubernetesSecretConfig from "./DeployNodeConfigFormKubernetesSecretConfig"; @@ -134,6 +135,8 @@ const DeployNodeConfigForm = forwardRef; case DEPLOY_PROVIDERS.DOGECLOUD_CDN: return ; + case DEPLOY_PROVIDERS.EDGIO_APPLICATIONS: + return ; case DEPLOY_PROVIDERS.HUAWEICLOUD_CDN: return ; case DEPLOY_PROVIDERS.HUAWEICLOUD_ELB: diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormEdgioApplicationsConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormEdgioApplicationsConfig.tsx new file mode 100644 index 00000000..2a6929d8 --- /dev/null +++ b/ui/src/components/workflow/node/DeployNodeConfigFormEdgioApplicationsConfig.tsx @@ -0,0 +1,65 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +type DeployNodeConfigFormEdgioApplicationsConfigFieldValues = Nullish<{ + environmentId: string; +}>; + +export type DeployNodeConfigFormEdgioApplicationsConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: DeployNodeConfigFormEdgioApplicationsConfigFieldValues; + onValuesChange?: (values: DeployNodeConfigFormEdgioApplicationsConfigFieldValues) => void; +}; + +const initFormModel = (): DeployNodeConfigFormEdgioApplicationsConfigFieldValues => { + return {}; +}; + +const DeployNodeConfigFormEdgioApplicationsConfig = ({ + form: formInst, + formName, + disabled, + initialValues, + onValuesChange, +}: DeployNodeConfigFormEdgioApplicationsConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + environmentId: z + .string({ message: t("workflow_node.deploy.form.edgio_applications_environment_id.placeholder") }) + .min(1, t("workflow_node.deploy.form.edgio_applications_environment_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })) + .trim(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + +
+ ); +}; + +export default DeployNodeConfigFormEdgioApplicationsConfig; diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts index ed2bd310..0c0d6986 100644 --- a/ui/src/domain/access.ts +++ b/ui/src/domain/access.ts @@ -16,6 +16,7 @@ export interface AccessModel extends BaseModel { | AccessConfigForBytePlus | AccessConfigForCloudflare | AccessConfigForDogeCloud + | AccessConfigForEdgio | AccessConfigForGoDaddy | AccessConfigForHuaweiCloud | AccessConfigForKubernetes @@ -77,6 +78,11 @@ export type AccessConfigForDogeCloud = { secretKey: string; }; +export type AccessConfigForEdgio = { + clientId: string; + clientSecret: string; +}; + export type AccessConfigForGoDaddy = { apiKey: string; apiSecret: string; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index ba1a10f2..574c022a 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -13,6 +13,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ CLOUDFLARE: "cloudflare", DOGECLOUD: "dogecloud", GODADDY: "godaddy", + EDGIO: "edgio", HUAWEICLOUD: "huaweicloud", KUBERNETES: "k8s", LOCAL: "local", @@ -64,6 +65,7 @@ export const accessProvidersMap: Map [ type, { diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json index d609d6e7..47f59120 100644 --- a/ui/src/i18n/locales/en/nls.access.json +++ b/ui/src/i18n/locales/en/nls.access.json @@ -78,6 +78,12 @@ "access.form.dogecloud_secret_key.label": "Doge Cloud SecretKey", "access.form.dogecloud_secret_key.placeholder": "Please enter Doge Cloud SecretKey", "access.form.dogecloud_secret_key.tooltip": "For more information, see https://console.dogecloud.com/", + "access.form.edgio_client_id.label": "Edgio ClientId", + "access.form.edgio_client_id.placeholder": "Please enter Edgio ClientId", + "access.form.edgio_client_id.tooltip": "For more information, see https://docs.edg.io/applications/v7/rest_api/authentication#administering-api-clients", + "access.form.edgio_client_secret.label": "Edgio ClientSecret", + "access.form.edgio_client_secret.placeholder": "Please enter Edgio ClientSecret", + "access.form.edgio_client_secret.tooltip": "For more information, see https://docs.edg.io/applications/v7/rest_api/authentication#administering-api-clients", "access.form.godaddy_api_key.label": "GoDaddy API key", "access.form.godaddy_api_key.placeholder": "Please enter GoDaddy API key", "access.form.godaddy_api_key.tooltip": "For more information, see https://developer.godaddy.com/", diff --git a/ui/src/i18n/locales/en/nls.common.json b/ui/src/i18n/locales/en/nls.common.json index 7b0199a9..53eeb515 100644 --- a/ui/src/i18n/locales/en/nls.common.json +++ b/ui/src/i18n/locales/en/nls.common.json @@ -56,6 +56,8 @@ "common.provider.cloudflare": "Cloudflare", "common.provider.dogecloud": "Doge Cloud", "common.provider.dogecloud.cdn": "Doge Cloud - Content Delivery Network (CDN)", + "common.provider.edgio": "Edgio", + "common.provider.edgio.applications": "Edgio - Applications", "common.provider.godaddy": "GoDaddy", "common.provider.huaweicloud": "Huawei Cloud", "common.provider.huaweicloud.cdn": "Huawei Cloud - Content Delivery Network (CDN)", diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 7275d08b..89d8ae71 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -148,6 +148,9 @@ "workflow_node.deploy.form.dogecloud_cdn_domain.label": "Doge Cloud CDN domain", "workflow_node.deploy.form.dogecloud_cdn_domain.placeholder": "Please enter Doge Cloud CDN domain name", "workflow_node.deploy.form.dogecloud_cdn_domain.tooltip": "For more information, see https://console.dogecloud.com/", + "workflow_node.deploy.form.edgio_applications_environment_id.label": "Edgio Applications environment ID", + "workflow_node.deploy.form.edgio_applications_environment_id.placeholder": "Please enter Edgio Applications environment ID", + "workflow_node.deploy.form.edgio_applications_environment_id.tooltip": "For more information, see https://edgio.app/", "workflow_node.deploy.form.huaweicloud_cdn_region.label": "Huawei Cloud region", "workflow_node.deploy.form.huaweicloud_cdn_region.placeholder": "Please enter Huawei Cloud region (e.g. cn-north-1)", "workflow_node.deploy.form.huaweicloud_cdn_region.tooltip": "For more information, see https://console-intl.huaweicloud.com/apiexplorer/#/endpoint", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index b9107581..dc8d8c93 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -78,6 +78,12 @@ "access.form.dogecloud_secret_key.label": "多吉云 SecretKey", "access.form.dogecloud_secret_key.placeholder": "请输入多吉云 SecretKey", "access.form.dogecloud_secret_key.tooltip": "这是什么?请参阅 https://console.dogecloud.com/", + "access.form.edgio_client_id.label": "Edgio 客户端 ID", + "access.form.edgio_client_id.placeholder": "请输入 Edgio 客户端 ID", + "access.form.edgio_client_id.tooltip": "这是什么?请参阅 https://docs.edg.io/applications/v7/rest_api/authentication#administering-api-clients", + "access.form.edgio_client_secret.label": "Edgio 客户端密码", + "access.form.edgio_client_secret.placeholder": "请输入 Edgio 客户端密码", + "access.form.edgio_client_secret.tooltip": "这是什么?请参阅 https://docs.edg.io/applications/v7/rest_api/authentication#administering-api-clients", "access.form.godaddy_api_key.label": "GoDaddy API Key", "access.form.godaddy_api_key.placeholder": "请输入 GoDaddy API Key", "access.form.godaddy_api_key.tooltip": "这是什么?请参阅 https://developer.godaddy.com/", diff --git a/ui/src/i18n/locales/zh/nls.common.json b/ui/src/i18n/locales/zh/nls.common.json index 0951152a..87fce337 100644 --- a/ui/src/i18n/locales/zh/nls.common.json +++ b/ui/src/i18n/locales/zh/nls.common.json @@ -56,6 +56,8 @@ "common.provider.cloudflare": "Cloudflare", "common.provider.dogecloud": "多吉云", "common.provider.dogecloud.cdn": "多吉云 - 内容分发网络 CDN", + "common.provider.edgio": "Edgio", + "common.provider.edgio.applications": "Edgio - Applications", "common.provider.godaddy": "GoDaddy", "common.provider.huaweicloud": "华为云", "common.provider.huaweicloud.cdn": "华为云 - 内容分发网络 CDN", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 00b24d75..b6291e06 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -148,6 +148,9 @@ "workflow_node.deploy.form.dogecloud_cdn_domain.label": "多吉云 CDN 加速域名", "workflow_node.deploy.form.dogecloud_cdn_domain.placeholder": "请输入多吉云 CDN 加速域名", "workflow_node.deploy.form.dogecloud_cdn_domain.tooltip": "这是什么?请参阅 https://console.dogecloud.com", + "workflow_node.deploy.form.edgio_applications_environment_id.label": "Edgio Applications 环境 ID", + "workflow_node.deploy.form.edgio_applications_environment_id.placeholder": "请输入 Edgio Applications 环境 ID", + "workflow_node.deploy.form.edgio_applications_environment_id.tooltip": "这是什么?请参阅 https://edgio.app/", "workflow_node.deploy.form.huaweicloud_cdn_region.label": "华为云区域", "workflow_node.deploy.form.huaweicloud_cdn_region.placeholder": "请输入华为云区域(例如:cn-north-1)", "workflow_node.deploy.form.huaweicloud_cdn_region.tooltip": "这是什么?请参阅 https://console.huaweicloud.com/apiexplorer/#/endpoint",