From 7528476b24127235b9639d635454cff0bd96007b Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Fri, 12 Jun 2026 11:38:30 +0800 Subject: [PATCH] feat(provider): new deployment provider: linode object storage --- internal/certmgmt/deployers/sp_linode_los.go | 26 +++++ internal/domain/provider.go | 1 + .../providers/linode-los/linode_los.go | 109 ++++++++++++++++++ .../providers/linode-los/linode_los_test.go | 53 +++++++++ .../linode/api_delete_object_storage_ssl.go | 40 +++++++ .../linode/api_get_object_storage_ssl.go | 42 +++++++ .../linode/api_upload_object_storage_ssl.go | 50 ++++++++ .../BizDeployNodeConfigFieldsProvider.tsx | 2 + ...eployNodeConfigFieldsProviderLinodeLOS.tsx | 63 ++++++++++ ui/src/domain/provider.ts | 100 ++++++++-------- ui/src/i18n/resources/en/nls.provider.json | 1 + .../i18n/resources/en/nls.workflow.nodes.json | 9 ++ ui/src/i18n/resources/zh/nls.provider.json | 1 + .../i18n/resources/zh/nls.workflow.nodes.json | 9 ++ 14 files changed, 457 insertions(+), 49 deletions(-) create mode 100644 internal/certmgmt/deployers/sp_linode_los.go create mode 100644 pkg/core/deployer/providers/linode-los/linode_los.go create mode 100644 pkg/core/deployer/providers/linode-los/linode_los_test.go create mode 100644 pkg/sdk3rd/linode/api_delete_object_storage_ssl.go create mode 100644 pkg/sdk3rd/linode/api_get_object_storage_ssl.go create mode 100644 pkg/sdk3rd/linode/api_upload_object_storage_ssl.go create mode 100644 ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderLinodeLOS.tsx diff --git a/internal/certmgmt/deployers/sp_linode_los.go b/internal/certmgmt/deployers/sp_linode_los.go new file mode 100644 index 000000000..a0d03fd07 --- /dev/null +++ b/internal/certmgmt/deployers/sp_linode_los.go @@ -0,0 +1,26 @@ +package deployers + +import ( + "fmt" + + "github.com/certimate-go/certimate/internal/domain" + "github.com/certimate-go/certimate/pkg/core" + dplyimpl "github.com/certimate-go/certimate/pkg/core/deployer/providers/linode-los" + xmaps "github.com/certimate-go/certimate/pkg/utils/maps" +) + +func init() { + Registries.MustRegister(domain.DeploymentProviderTypeLinodeLOS, func(options *ProviderFactoryOptions) (core.Deployer, 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 := dplyimpl.NewDeployer(&dplyimpl.DeployerConfig{ + AccessToken: credentials.AccessToken, + RegionId: xmaps.GetString(options.ProviderExtendedConfig, "regionId"), + Bucket: xmaps.GetString(options.ProviderExtendedConfig, "bucket"), + }) + return provider, err + }) +} diff --git a/internal/domain/provider.go b/internal/domain/provider.go index f96ef9159..bebfea7ea 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -391,6 +391,7 @@ const ( DeploymentProviderTypeKsyunCDN = DeploymentProviderType(AccessProviderTypeKsyun + "-cdn") DeploymentProviderTypeKsyunSLB = DeploymentProviderType(AccessProviderTypeKsyun + "-slb") DeploymentProviderTypeLeCDN = DeploymentProviderType(AccessProviderTypeLeCDN) + DeploymentProviderTypeLinodeLOS = DeploymentProviderType(AccessProviderTypeLinode + "-los") DeploymentProviderTypeLocal = DeploymentProviderType(AccessProviderTypeLocal) DeploymentProviderTypeMohuaMVH = DeploymentProviderType(AccessProviderTypeMohua + "-mvh") DeploymentProviderTypeNetlify = DeploymentProviderType(AccessProviderTypeNetlify) diff --git a/pkg/core/deployer/providers/linode-los/linode_los.go b/pkg/core/deployer/providers/linode-los/linode_los.go new file mode 100644 index 000000000..1f6d6ee7d --- /dev/null +++ b/pkg/core/deployer/providers/linode-los/linode_los.go @@ -0,0 +1,109 @@ +package linodelos + +import ( + "context" + "fmt" + "log/slog" + + "github.com/certimate-go/certimate/pkg/core" + linodesdk "github.com/certimate-go/certimate/pkg/sdk3rd/linode" +) + +type ( + Provider = core.Deployer + DeployResult = core.DeployerDeployResult +) + +type DeployerConfig struct { + // Linode AccessToken。 + AccessToken string `json:"accessToken"` + // 对象存储区域 ID。 + RegionId string `json:"regionId"` + // 对象存储桶名。 + Bucket string `json:"bucket"` +} + +type Deployer struct { + config *DeployerConfig + logger *slog.Logger + sdkClient *linodesdk.Client +} + +var _ Provider = (*Deployer)(nil) + +func NewDeployer(config *DeployerConfig) (*Deployer, error) { + if config == nil { + return nil, fmt.Errorf("the configuration of the deployer provider is nil") + } + + client, err := createSDKClient(config.AccessToken) + if err != nil { + return nil, fmt.Errorf("could not create client: %w", err) + } + + return &Deployer{ + config: config, + logger: slog.Default(), + sdkClient: client, + }, nil +} + +func (d *Deployer) SetLogger(logger *slog.Logger) { + if logger == nil { + d.logger = slog.New(slog.DiscardHandler) + } else { + d.logger = logger + } +} + +func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*DeployResult, error) { + if d.config.RegionId == "" { + return nil, fmt.Errorf("config `regionId` is required") + } + if d.config.Bucket == "" { + return nil, fmt.Errorf("config `bucket` is required") + } + + // Get an Object Storage TLS/SSL certificate + // REF: https://techdocs.akamai.com/linode-api/reference/get-object-storage-ssl + getObjectStorageSSLResp, err := d.sdkClient.GetObjectStorageSSLWithContext(ctx, d.config.RegionId, d.config.Bucket) + d.logger.Debug("sdk request 'objectstorage.GetSSL'", slog.String("params.regionId", d.config.RegionId), slog.String("params.bucket", d.config.Bucket), slog.Any("response", getObjectStorageSSLResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'objectstorage.GetSSL': %w", err) + } + + // Delete an Object Storage TLS/SSL certificate + // REF: https://techdocs.akamai.com/linode-api/reference/delete-object-storage-ssl + if getObjectStorageSSLResp.SSL { + deleteObjectStorageSSLResp, err := d.sdkClient.DeleteObjectStorageSSLWithContext(ctx, d.config.RegionId, d.config.Bucket) + d.logger.Debug("sdk request 'objectstorage.DeleteSSL'", slog.String("params.regionId", d.config.RegionId), slog.String("params.bucket", d.config.Bucket), slog.Any("response", deleteObjectStorageSSLResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'objectstorage.DeleteSSL': %w", err) + } + } + + // Upload an Object Storage TLS/SSL certificate + // REF: https://techdocs.akamai.com/linode-api/reference/post-object-storage-ssl + uploadObjectStorageSSLReq := &linodesdk.UploadObjectStorageSSLRequest{ + Certificate: certPEM, + PrivateKey: privkeyPEM, + } + uploadObjectStorageSSLResp, err := d.sdkClient.UploadObjectStorageSSLWithContext(ctx, d.config.RegionId, d.config.Bucket, uploadObjectStorageSSLReq) + d.logger.Debug("sdk request 'objectstorage.UploadSSL'", slog.String("params.regionId", d.config.RegionId), slog.String("params.bucket", d.config.Bucket), slog.Any("request", uploadObjectStorageSSLReq), slog.Any("response", uploadObjectStorageSSLResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'objectstorage.UploadSSL': %w", err) + } + + return &DeployResult{}, nil +} + +func createSDKClient(accessToken string) (*linodesdk.Client, error) { + client, err := linodesdk.NewClient( + linodesdk.WithAccessToken(accessToken), + ) + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/pkg/core/deployer/providers/linode-los/linode_los_test.go b/pkg/core/deployer/providers/linode-los/linode_los_test.go new file mode 100644 index 000000000..904b9ccb9 --- /dev/null +++ b/pkg/core/deployer/providers/linode-los/linode_los_test.go @@ -0,0 +1,53 @@ +package linodelos_test + +import ( + "testing" + + "github.com/certimate-go/certimate/pkg/core/deployer/internal/tester" + impl "github.com/certimate-go/certimate/pkg/core/deployer/providers/linode-los" +) + +var ( + fp = tester.Args("LINODELOS_") + fTestCertPath string + fTestKeyPath string + fApiToken string + fAppName string + fDomain string +) + +func init() { + fp.DefineString(&fTestCertPath, "TESTCERTPATH") + fp.DefineString(&fTestKeyPath, "TESTKEYPATH") + fp.DefineString(&fApiToken, "APITOKEN") + fp.DefineString(&fAppName, "APPNAME") + fp.DefineString(&fDomain, "DOMAIN") +} + +/* +Shell command to run this test: + + go test -v ./linode_los_test.go -args \ + --LINODELOS_TESTCERTPATH="/path/to/your-test-cert.pem" \ + --LINODELOS_TESTKEYPATH="/path/to/your-test-key.pem" \ + --LINODELOS_APITOKEN="your-api-token" \ + --LINODELOS_APPNAME="your-app-name" \ + --LINODELOS_DOMAIN="example.com" +*/ +func TestProvider(t *testing.T) { + fp.Parse() + + t.Run("Deploy", func(t *testing.T) { + provider, err := impl.NewDeployer(&impl.DeployerConfig{ + ApiToken: fApiToken, + AppName: fAppName, + Domain: fDomain, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + tester.TestDeploy(t, provider, tester.TestDeployArgs{CertPath: fTestCertPath, KeyPath: fTestKeyPath}) + }) +} diff --git a/pkg/sdk3rd/linode/api_delete_object_storage_ssl.go b/pkg/sdk3rd/linode/api_delete_object_storage_ssl.go new file mode 100644 index 000000000..e92dd2dc7 --- /dev/null +++ b/pkg/sdk3rd/linode/api_delete_object_storage_ssl.go @@ -0,0 +1,40 @@ +package linode + +import ( + "context" + "fmt" + "net/http" + "net/url" +) + +type DeleteObjectStorageSSLResponse struct { + sdkResponseBase +} + +func (c *Client) DeleteObjectStorageSSL(regionId string, bucket string) (*DeleteObjectStorageSSLResponse, error) { + return c.DeleteObjectStorageSSLWithContext(context.Background(), regionId, bucket) +} + +func (c *Client) DeleteObjectStorageSSLWithContext(ctx context.Context, regionId string, bucket string) (*DeleteObjectStorageSSLResponse, error) { + if regionId == "" { + return nil, fmt.Errorf("sdkerr: bad request: unset regionId") + } + if bucket == "" { + return nil, fmt.Errorf("sdkerr: bad request: unset bucket") + } + + path := fmt.Sprintf("/object-storage/buckets/%s/%s/ssl", url.PathEscape(regionId), url.PathEscape(bucket)) + httpreq, err := c.newRequest(http.MethodDelete, path) + if err != nil { + return nil, err + } else { + httpreq.SetContext(ctx) + } + + result := &DeleteObjectStorageSSLResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/linode/api_get_object_storage_ssl.go b/pkg/sdk3rd/linode/api_get_object_storage_ssl.go new file mode 100644 index 000000000..7bea6211c --- /dev/null +++ b/pkg/sdk3rd/linode/api_get_object_storage_ssl.go @@ -0,0 +1,42 @@ +package linode + +import ( + "context" + "fmt" + "net/http" + "net/url" +) + +type GetObjectStorageSSLResponse struct { + sdkResponseBase + + SSL bool `json:"ssl"` +} + +func (c *Client) GetObjectStorageSSL(regionId string, bucket string) (*GetObjectStorageSSLResponse, error) { + return c.GetObjectStorageSSLWithContext(context.Background(), regionId, bucket) +} + +func (c *Client) GetObjectStorageSSLWithContext(ctx context.Context, regionId string, bucket string) (*GetObjectStorageSSLResponse, error) { + if regionId == "" { + return nil, fmt.Errorf("sdkerr: bad request: unset regionId") + } + if bucket == "" { + return nil, fmt.Errorf("sdkerr: bad request: unset bucket") + } + + path := fmt.Sprintf("/object-storage/buckets/%s/%s/ssl", url.PathEscape(regionId), url.PathEscape(bucket)) + httpreq, err := c.newRequest(http.MethodGet, path) + if err != nil { + return nil, err + } else { + httpreq.SetContext(ctx) + } + + result := &GetObjectStorageSSLResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/linode/api_upload_object_storage_ssl.go b/pkg/sdk3rd/linode/api_upload_object_storage_ssl.go new file mode 100644 index 000000000..8a1a6eaf6 --- /dev/null +++ b/pkg/sdk3rd/linode/api_upload_object_storage_ssl.go @@ -0,0 +1,50 @@ +package linode + +import ( + "context" + "fmt" + "net/http" + "net/url" +) + +type UploadObjectStorageSSLRequest struct { + sdkResponseBase + + Certificate string `json:"certificate"` + PrivateKey string `json:"private_key"` +} + +type UploadObjectStorageSSLResponse struct { + sdkResponseBase + + SSL bool `json:"ssl"` +} + +func (c *Client) UploadObjectStorageSSL(regionId string, bucket string, req *UploadObjectStorageSSLRequest) (*UploadObjectStorageSSLResponse, error) { + return c.UploadObjectStorageSSLWithContext(context.Background(), regionId, bucket, req) +} + +func (c *Client) UploadObjectStorageSSLWithContext(ctx context.Context, regionId string, bucket string, req *UploadObjectStorageSSLRequest) (*UploadObjectStorageSSLResponse, error) { + if regionId == "" { + return nil, fmt.Errorf("sdkerr: bad request: unset regionId") + } + if bucket == "" { + return nil, fmt.Errorf("sdkerr: bad request: unset bucket") + } + + path := fmt.Sprintf("/object-storage/buckets/%s/%s/ssl", url.PathEscape(regionId), url.PathEscape(bucket)) + httpreq, err := c.newRequest(http.MethodPost, path) + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &UploadObjectStorageSSLResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx index bd0025217..4a151c5bd 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx @@ -81,6 +81,7 @@ import BizDeployNodeConfigFieldsProviderKsyunCDN from "./BizDeployNodeConfigFiel import BizDeployNodeConfigFieldsProviderKsyunSLB from "./BizDeployNodeConfigFieldsProviderKsyunSLB"; import BizDeployNodeConfigFieldsProviderKubernetesSecret from "./BizDeployNodeConfigFieldsProviderKubernetesSecret"; import BizDeployNodeConfigFieldsProviderLeCDN from "./BizDeployNodeConfigFieldsProviderLeCDN"; +import BizDeployNodeConfigFieldsProviderLinodeLOS from "./BizDeployNodeConfigFieldsProviderLinodeLOS"; import BizDeployNodeConfigFieldsProviderLocal from "./BizDeployNodeConfigFieldsProviderLocal"; import BizDeployNodeConfigFieldsProviderMohuaMVH from "./BizDeployNodeConfigFieldsProviderMohuaMVH"; import BizDeployNodeConfigFieldsProviderNetlify from "./BizDeployNodeConfigFieldsProviderNetlify"; @@ -223,6 +224,7 @@ const providerComponentMap: Partial { + const { i18n, t } = useTranslation(); + + const { parentNamePath } = useFormNestedFieldsContext(); + const formSchema = z.object({ + [parentNamePath]: getSchema({ i18n }), + }); + const formRule = createSchemaFieldRule(formSchema); + const initialValues = getInitialValues(); + + return ( + <> + } + > + + + + + + + + ); +}; + +const getInitialValues = (): Nullish>> => { + return { + regionId: "", + bucket: "", + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { + const { t: _ } = i18n; + + return z.object({ + regionId: z.string().nonempty(), + bucket: z.string().nonempty(), + }); +}; + +const _default = Object.assign(BizDeployNodeConfigFieldsProviderLinodeLOS, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 96ace1506..72e967bc3 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -168,7 +168,6 @@ export const accessProvidersMap: MapRequired when using administrator's authorization. It Must be the same as the user to which the certificate belongs." }, + "linode_los_region_id": { + "label": "Linode region ID", + "placeholder": "Please enter Linode object storage region ID (e.g. us-east)", + "tooltip": "For more information, see https://techdocs.akamai.com/cloud-computing/docs/endpoint-types" + }, + "linode_los_bucket": { + "label": "Linode object storage bucket name", + "placeholder": "Please enter Linode object storage bucket name" + }, "local": { "guide": "If you are running Certimate in Docker, the \"Local\" refers to the container rather than the host." }, diff --git a/ui/src/i18n/resources/zh/nls.provider.json b/ui/src/i18n/resources/zh/nls.provider.json index 485a01d9e..9d23d2302 100644 --- a/ui/src/i18n/resources/zh/nls.provider.json +++ b/ui/src/i18n/resources/zh/nls.provider.json @@ -187,6 +187,7 @@ "letsencrypt": "Let's Encrypt", "letsencryptstaging": "Let's Encrypt 测试环境", "linode": "Linode", + "linode_los": "Linode - 对象存储", "litessl": "LiteSSL", "local": "本地主机", "mattermost": "Mattermost", diff --git a/ui/src/i18n/resources/zh/nls.workflow.nodes.json b/ui/src/i18n/resources/zh/nls.workflow.nodes.json index b5e9ed598..8969f1679 100644 --- a/ui/src/i18n/resources/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/resources/zh/nls.workflow.nodes.json @@ -1830,6 +1830,15 @@ "placeholder": "请输入 LeCDN 客户 ID", "tooltip": "请登录 LeCDN 控制台查看。
使用的是系统管理员的授权信息时必填,需与证书所属客户相同。" }, + "linode_los_region_id": { + "label": "Linode 服务区域 ID", + "placeholder": "请输入 Linode 对象存储区域 ID(例如:us-east)", + "tooltip": "这是什么?请参阅 https://techdocs.akamai.com/cloud-computing/docs/endpoint-types" + }, + "linode_los_bucket": { + "label": "Linode 对象存储桶名", + "placeholder": "请输入 Linode 对象存储桶名" + }, "local": { "guide": "如果你正在使用 Docker 运行 Certimate,「本地」指的是容器内而非宿主机。" },