From 94eb0b0489f02a5a6c371a3f7aa4d9ec237dcfb4 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 5 Aug 2026 23:37:57 +0800 Subject: [PATCH] feat(provider): new deployment provider: axisnow --- internal/certmgmt/deployers/sp_axisnow.go | 24 +++ internal/domain/access.go | 4 + internal/domain/provider.go | 2 + pkg/core/certmgr/providers/axisnow/axisnow.go | 138 ++++++++++++++++++ .../certmgr/providers/axisnow/axisnow_test.go | 44 ++++++ .../deployer/providers/axisnow/axisnow.go | 69 +++++++++ pkg/sdk3rd/axisnow/api_add_certificate.go | 40 +++++ pkg/sdk3rd/axisnow/api_list_certificates.go | 53 +++++++ pkg/sdk3rd/axisnow/client.go | 100 +++++++++++++ pkg/sdk3rd/axisnow/models.go | 29 ++++ pkg/sdk3rd/axisnow/options.go | 13 ++ pkg/sdk3rd/axisnow/types.go | 46 ++++++ ui/public/imgs/providers/axisnow.png | Bin 0 -> 33292 bytes .../forms/AccessConfigFieldsProvider.tsx | 2 + .../AccessConfigFieldsProviderAxisNow.tsx | 52 +++++++ ui/src/domain/provider.ts | 4 + ui/src/i18n/resources/en/nls.access.json | 5 + ui/src/i18n/resources/en/nls.provider.json | 1 + ui/src/i18n/resources/zh/nls.access.json | 5 + ui/src/i18n/resources/zh/nls.provider.json | 1 + 20 files changed, 632 insertions(+) create mode 100644 internal/certmgmt/deployers/sp_axisnow.go create mode 100644 pkg/core/certmgr/providers/axisnow/axisnow.go create mode 100644 pkg/core/certmgr/providers/axisnow/axisnow_test.go create mode 100644 pkg/core/deployer/providers/axisnow/axisnow.go create mode 100644 pkg/sdk3rd/axisnow/api_add_certificate.go create mode 100644 pkg/sdk3rd/axisnow/api_list_certificates.go create mode 100644 pkg/sdk3rd/axisnow/client.go create mode 100644 pkg/sdk3rd/axisnow/models.go create mode 100644 pkg/sdk3rd/axisnow/options.go create mode 100644 pkg/sdk3rd/axisnow/types.go create mode 100644 ui/public/imgs/providers/axisnow.png create mode 100644 ui/src/components/access/forms/AccessConfigFieldsProviderAxisNow.tsx diff --git a/internal/certmgmt/deployers/sp_axisnow.go b/internal/certmgmt/deployers/sp_axisnow.go new file mode 100644 index 000000000..48d114a1a --- /dev/null +++ b/internal/certmgmt/deployers/sp_axisnow.go @@ -0,0 +1,24 @@ +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/axisnow" + xmaps "github.com/certimate-go/certimate/pkg/utils/maps" +) + +func init() { + Registries.MustRegister(domain.DeploymentProviderTypeAxisNow, func(options *ProviderFactoryOptions) (core.Deployer, error) { + credentials := domain.AccessConfigForAxisNow{} + if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + provider, err := dplyimpl.NewDeployer(&dplyimpl.DeployerConfig{ + ApiToken: credentials.ApiToken, + }) + return provider, err + }) +} diff --git a/internal/domain/access.go b/internal/domain/access.go index a7fb37b22..a2ddf4487 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -87,6 +87,10 @@ type AccessConfigForAWS struct { SecretAccessKey string `json:"secretAccessKey"` } +type AccessConfigForAxisNow struct { + ApiToken string `json:"apiToken"` +} + type AccessConfigForAzure struct { TenantId string `json:"tenantId"` ClientId string `json:"clientId"` diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 707705cbb..51931f0b6 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -25,6 +25,7 @@ const ( AccessProviderTypeAPISIX = AccessProviderType("apisix") AccessProviderTypeArvanCloud = AccessProviderType("arvancloud") AccessProviderTypeAWS = AccessProviderType("aws") + AccessProviderTypeAxisNow = AccessProviderType("axisnow") AccessProviderTypeAzure = AccessProviderType("azure") AccessProviderTypeBaiduCloud = AccessProviderType("baiducloud") AccessProviderTypeBaishan = AccessProviderType("baishan") @@ -342,6 +343,7 @@ const ( DeploymentProviderTypeAWSCloudFront = DeploymentProviderType(AccessProviderTypeAWS + "-cloudfront") DeploymentProviderTypeAWSIAM = DeploymentProviderType(AccessProviderTypeAWS + "-iam") DeploymentProviderTypeAWSNLB = DeploymentProviderType(AccessProviderTypeAWS + "-nlb") + DeploymentProviderTypeAxisNow = DeploymentProviderType(AccessProviderTypeAxisNow) DeploymentProviderTypeAzureKeyVault = DeploymentProviderType(AccessProviderTypeAzure + "-keyvault") DeploymentProviderTypeBaiduCloudAppBLB = DeploymentProviderType(AccessProviderTypeBaiduCloud + "-appblb") DeploymentProviderTypeBaiduCloudBLB = DeploymentProviderType(AccessProviderTypeBaiduCloud + "-blb") diff --git a/pkg/core/certmgr/providers/axisnow/axisnow.go b/pkg/core/certmgr/providers/axisnow/axisnow.go new file mode 100644 index 000000000..92ceaefed --- /dev/null +++ b/pkg/core/certmgr/providers/axisnow/axisnow.go @@ -0,0 +1,138 @@ +package axisnow + +import ( + "context" + "fmt" + "log/slog" + "time" + + "github.com/samber/lo" + + "github.com/certimate-go/certimate/pkg/core" + axisnowsdk "github.com/certimate-go/certimate/pkg/sdk3rd/axisnow" + xcert "github.com/certimate-go/certimate/pkg/utils/cert" +) + +type ( + Provider = core.Certmgr + UploadResult = core.CertmgrUploadResult + ReplaceResult = core.CertmgrReplaceResult +) + +type CertmgrConfig struct { + // AxisNow API 令牌。 + ApiToken string `json:"apiToken"` +} + +type Certmgr struct { + config *CertmgrConfig + logger *slog.Logger + sdkClient *axisnowsdk.Client +} + +var _ Provider = (*Certmgr)(nil) + +func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { + if config == nil { + return nil, fmt.Errorf("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) (*UploadResult, error) { + // 查询证书列表,避免重复上传 + // REF: https://developers.axisnow.io/api-reference/main/client/v1/#/operations/certificate-setting-list-certificates + listCertificatesPage := 1 + listCertificatesPerPage := 100 + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + listCertificatesReq := &axisnowsdk.ListCertificatesRequest{ + Page: lo.ToPtr(listCertificatesPage), + PerPage: lo.ToPtr(listCertificatesPerPage), + } + listCertificatesResp, err := c.sdkClient.ListCertificatesWithContext(ctx, listCertificatesReq) + c.logger.Debug("sdk request 'Certificates.ListCertificates'", slog.Any("request", listCertificatesReq), slog.Any("response", listCertificatesResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'Certificates.ListCertificates': %w", err) + } else { + if listCertificatesResp.Results == nil { + break + } + + for _, certItem := range listCertificatesResp.Results { + if xcert.EqualCertificatesFromPEM(certPEM, certItem.Certificate) { + // 如果已存在相同证书,直接返回 + c.logger.Info("ssl certificate already exists") + return &UploadResult{ + CertId: certItem.UUID, + CertName: certItem.Name, + }, nil + } + } + + if len(listCertificatesResp.Results) < listCertificatesPerPage || + (listCertificatesResp.ResultInfo != nil && listCertificatesPage*listCertificatesPerPage >= listCertificatesResp.ResultInfo.TotalCount) { + break + } + + listCertificatesPage++ + } + } + + // 添加证书 + // REF: https://developers.axisnow.io/api-reference/main/client/v1/#/operations/certificate-setting-add-certificate + addCertificateReq := &axisnowsdk.AddCertificateRequest{ + Type: lo.ToPtr("upload"), + Name: lo.ToPtr(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())), + Certificate: lo.ToPtr(certPEM), + PrivateKey: lo.ToPtr(privkeyPEM), + } + addCertificateResp, err := c.sdkClient.AddCertificateWithContext(ctx, addCertificateReq) + c.logger.Debug("sdk request 'Certificates.AddCertificate'", slog.Any("request", addCertificateReq), slog.Any("response", addCertificateResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'Certificates.AddCertificate': %w", err) + } + + return &UploadResult{ + CertId: addCertificateResp.Result.UUID, + CertName: addCertificateResp.Result.Name, + }, nil +} + +func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*ReplaceResult, error) { + return nil, core.ErrUnsupported +} + +func createSDKClient(apiToken string) (*axisnowsdk.Client, error) { + client, err := axisnowsdk.NewClient( + axisnowsdk.WithApiToken(apiToken), + ) + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/pkg/core/certmgr/providers/axisnow/axisnow_test.go b/pkg/core/certmgr/providers/axisnow/axisnow_test.go new file mode 100644 index 000000000..2870243ce --- /dev/null +++ b/pkg/core/certmgr/providers/axisnow/axisnow_test.go @@ -0,0 +1,44 @@ +package axisnow_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + impl "github.com/certimate-go/certimate/pkg/core/certmgr/providers/axisnow" + it "github.com/certimate-go/certimate/pkg/core/certmgr/testing" +) + +var ( + fp = it.Args("AXISNOW_") + fTestCertPath string + fTestKeyPath string + fApiToken string +) + +func init() { + fp.DefineString(&fTestCertPath, "TESTCERTPATH") + fp.DefineString(&fTestKeyPath, "TESTKEYPATH") + fp.DefineString(&fApiToken, "APITOKEN") +} + +/* +Shell command to run this test: + + go test -v ./axisnow_test.go -args \ + --AXISNOW_TESTCERTPATH="/path/to/your-test-cert.pem" \ + --AXISNOW_TESTKEYPATH="/path/to/your-test-key.pem" \ + --AXISNOW_APITOKEN="your-api-token" +*/ +func TestProvider(t *testing.T) { + fp.Parse() + + t.Run("Upload", func(t *testing.T) { + provider, err := impl.NewCertmgr(&impl.CertmgrConfig{ + ApiToken: fApiToken, + }) + require.NoError(t, err) + + it.TestUpload(t, provider, it.TestUploadArgs{CertPath: fTestCertPath, KeyPath: fTestKeyPath}) + }) +} diff --git a/pkg/core/deployer/providers/axisnow/axisnow.go b/pkg/core/deployer/providers/axisnow/axisnow.go new file mode 100644 index 000000000..b56b619f1 --- /dev/null +++ b/pkg/core/deployer/providers/axisnow/axisnow.go @@ -0,0 +1,69 @@ +package axisnow + +import ( + "context" + "fmt" + "log/slog" + + "github.com/certimate-go/certimate/pkg/core" + cmgrimpl "github.com/certimate-go/certimate/pkg/core/certmgr/providers/axisnow" +) + +type ( + Provider = core.Deployer + DeployResult = core.DeployerDeployResult +) + +type DeployerConfig struct { + // AxisNow API 令牌。 + ApiToken string `json:"apiToken"` +} + +type Deployer struct { + config *DeployerConfig + logger *slog.Logger + sdkCertmgr core.Certmgr +} + +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") + } + + pcertmgr, err := cmgrimpl.NewCertmgr(&cmgrimpl.CertmgrConfig{ + ApiToken: config.ApiToken, + }) + 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) (*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 &DeployResult{}, nil +} diff --git a/pkg/sdk3rd/axisnow/api_add_certificate.go b/pkg/sdk3rd/axisnow/api_add_certificate.go new file mode 100644 index 000000000..b708e651d --- /dev/null +++ b/pkg/sdk3rd/axisnow/api_add_certificate.go @@ -0,0 +1,40 @@ +package axisnow + +import ( + "context" + "net/http" +) + +type AddCertificateRequest struct { + Name *string `json:"name,omitempty"` + Type *string `json:"type,omitempty"` + Certificate *string `json:"certificate,omitempty"` + PrivateKey *string `json:"private_key,omitempty"` +} + +type AddCertificateResponse struct { + sdkResponseBase + + Result *Certificate `json:"result,omitempty"` +} + +func (c *Client) AddCertificate(req *AddCertificateRequest) (*AddCertificateResponse, error) { + return c.AddCertificateWithContext(context.Background(), req) +} + +func (c *Client) AddCertificateWithContext(ctx context.Context, req *AddCertificateRequest) (*AddCertificateResponse, error) { + httpreq, err := c.newRequest(http.MethodPost, "/certificates") + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &AddCertificateResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/axisnow/api_list_certificates.go b/pkg/sdk3rd/axisnow/api_list_certificates.go new file mode 100644 index 000000000..06e8233e9 --- /dev/null +++ b/pkg/sdk3rd/axisnow/api_list_certificates.go @@ -0,0 +1,53 @@ +package axisnow + +import ( + "context" + "net/http" + + qs "github.com/google/go-querystring/query" +) + +type ListCertificatesRequest struct { + Filter *string `json:"filter,omitempty" url:"filter,omitempty"` + Page *int `json:"page,omitempty" url:"page,omitempty"` + PerPage *int `json:"per_page,omitempty" url:"per_page,omitempty"` +} + +type ListCertificatesResponse struct { + sdkResponseBase + + ResultInfo *struct { + Page int `json:"page"` + PerPage int `json:"per_page"` + Count int `json:"count"` + TotalCount int `json:"total_count"` + SubscribedTotal int `json:"subscribed_total"` + } `json:"result_info,omitempty"` + Results []*Certificate `json:"result,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, "/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 := &ListCertificatesResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/axisnow/client.go b/pkg/sdk3rd/axisnow/client.go new file mode 100644 index 000000000..fc0cf3a97 --- /dev/null +++ b/pkg/sdk3rd/axisnow/client.go @@ -0,0 +1,100 @@ +// A simple SDK client for AxisNow. +// API documentation: https://developers.axisnow.io/api-reference/main/client/v1/#/ +package axisnow + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/go-resty/resty/v2" + + "github.com/certimate-go/certimate/internal/app" +) + +type Client struct { + rc *resty.Client +} + +func NewClient(optFns ...OptionsFunc) (*Client, error) { + opts := &Options{} + for _, fn := range optFns { + fn(opts) + } + + if opts.ApiToken == "" { + return nil, fmt.Errorf("sdkerr: unset apiToken") + } + + httper := resty.New(). + SetBaseURL("https://api.axisnow.io/client/v1"). + SetHeader("Accept", "application/json"). + SetHeader("Authorization", "Bearer "+opts.ApiToken). + SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", app.AppUserAgent) + + return &Client{rc: httper}, nil +} + +func (c *Client) SetTimeout(timeout time.Duration) *Client { + c.rc.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.rc.R() + req.Method = method + req.URL = path + + // WARN: + // DO NOT CALL `req.SetResult` or `req.SetError` AGAIN! USE `doRequestWithResult` INSTEAD. + return req, nil +} + +func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + 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 err := res.GetAPIError(); err != nil { + return resp, fmt.Errorf("sdkerr: api error: %s", err.Error()) + } + } + } + + return resp, nil +} diff --git a/pkg/sdk3rd/axisnow/models.go b/pkg/sdk3rd/axisnow/models.go new file mode 100644 index 000000000..d46265cd0 --- /dev/null +++ b/pkg/sdk3rd/axisnow/models.go @@ -0,0 +1,29 @@ +package axisnow + +type Certificate struct { + UUID string `json:"uuid,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + SubjectName []string `json:"subject_name,omitempty"` + Issuer CertificateIssuer `json:"issuer,omitempty"` + Subject CertificateSubject `json:"subject,omitempty"` + IssueTime int64 `json:"issue_time,omitempty"` + ExpireTime int64 `json:"expire_time,omitempty"` + Certificate string `json:"certificate,omitempty"` + ReferencedCount int `json:"referenced_count,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` +} + +type CertificateIssuer struct { + Country string `json:"country,omitempty"` + Organization string `json:"organization,omitempty"` + OrganizationUnit string `json:"organization_unit,omitempty"` + CommonName string `json:"common_name,omitempty"` +} + +type CertificateSubject struct { + Organization string `json:"organization,omitempty"` + OrganizationUnit string `json:"organization_unit,omitempty"` + CommonName string `json:"common_name,omitempty"` +} diff --git a/pkg/sdk3rd/axisnow/options.go b/pkg/sdk3rd/axisnow/options.go new file mode 100644 index 000000000..038ebdff1 --- /dev/null +++ b/pkg/sdk3rd/axisnow/options.go @@ -0,0 +1,13 @@ +package axisnow + +type Options struct { + ApiToken string +} + +type OptionsFunc func(*Options) + +func WithApiToken(apiToken string) OptionsFunc { + return func(o *Options) { + o.ApiToken = apiToken + } +} diff --git a/pkg/sdk3rd/axisnow/types.go b/pkg/sdk3rd/axisnow/types.go new file mode 100644 index 000000000..f26f4d09a --- /dev/null +++ b/pkg/sdk3rd/axisnow/types.go @@ -0,0 +1,46 @@ +package axisnow + +import ( + "fmt" + "strings" +) + +type sdkResponse interface { + GetAPIError() error + GetSuccess() bool +} + +type sdkResponseBase struct { + Success bool `json:"success"` + Errors sdkAPIErrors `json:"errors,omitempty"` +} + +type sdkAPIError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type sdkAPIErrors []sdkAPIError + +func (e sdkAPIErrors) Error() string { + builder := &strings.Builder{} + + for _, item := range e { + fmt.Fprintf(builder, "[%d] %s", item.Code, item.Message) + } + + return strings.TrimSpace(builder.String()) +} + +func (r *sdkResponseBase) GetSuccess() bool { + return r.Success +} + +func (r *sdkResponseBase) GetAPIError() error { + if len(r.Errors) > 0 { + return r.Errors + } + return nil +} + +var _ sdkResponse = (*sdkResponseBase)(nil) diff --git a/ui/public/imgs/providers/axisnow.png b/ui/public/imgs/providers/axisnow.png new file mode 100644 index 0000000000000000000000000000000000000000..bea9f00b1a2f2372937a58284fc3558ed4ae8f30 GIT binary patch literal 33292 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 iXu)0l3&-OB3orotm!%R576ljp0000 { + 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(), + }); +}; + +const _default = Object.assign(AccessConfigFormFieldsProviderAxisNow, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 100e63c32..ee113aab6 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -29,6 +29,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ APISIX: "apisix", ARVANCLOUD: "arvancloud", AWS: "aws", + AXISNOW: "axisnow", AZURE: "azure", BAIDUCLOUD: "baiducloud", BAISHAN: "baishan", @@ -216,6 +217,7 @@ export const accessProvidersMap: Maphttps://docs.aws.amazon.com/en_us/IAM/latest/UserGuide/id_credentials_access-keys.html" }, + "axisnow_api_token": { + "label": "AxisNow API token", + "placeholder": "Please enter AxisNow API token", + "tooltip": "For more information, see https://www.axisnow.io/zh/docs/edge/tenant/api-tokens" + }, "azure_tenant_id": { "label": "Azure tenant ID", "placeholder": "Please enter Azure tenant ID", diff --git a/ui/src/i18n/resources/en/nls.provider.json b/ui/src/i18n/resources/en/nls.provider.json index 8b33868d2..a12c53bae 100644 --- a/ui/src/i18n/resources/en/nls.provider.json +++ b/ui/src/i18n/resources/en/nls.provider.json @@ -69,6 +69,7 @@ "aws_lightsail": "AWS - Lightsail", "aws_nlb": "AWS - NLB (Network Load Balancer)", "aws_route53": "AWS - Route53", + "axisnow": "AxisNow", "azure": "Azure (Microsoft Azure)", "azure_dns": "Azure - DNS", "azure_keyvault": "Azure - KeyVault", diff --git a/ui/src/i18n/resources/zh/nls.access.json b/ui/src/i18n/resources/zh/nls.access.json index 60dffd138..ad045dee6 100644 --- a/ui/src/i18n/resources/zh/nls.access.json +++ b/ui/src/i18n/resources/zh/nls.access.json @@ -243,6 +243,11 @@ "placeholder": "请输入 AWS SecretAccessKey", "tooltip": "这是什么?请参阅 https://docs.aws.amazon.com/zh_cn/IAM/latest/UserGuide/id_credentials_access-keys.html" }, + "axisnow_api_token": { + "label": "AxisNow API 令牌", + "placeholder": "请输入 AxisNow API 令牌", + "tooltip": "这是什么?请参阅 https://www.axisnow.io/zh/docs/edge/tenant/api-tokens" + }, "azure_tenant_id": { "label": "Azure 租户 ID", "placeholder": "请输入 Azure 租户 ID", diff --git a/ui/src/i18n/resources/zh/nls.provider.json b/ui/src/i18n/resources/zh/nls.provider.json index 9d3396023..11166a60c 100644 --- a/ui/src/i18n/resources/zh/nls.provider.json +++ b/ui/src/i18n/resources/zh/nls.provider.json @@ -69,6 +69,7 @@ "aws_lightsail": "AWS - Lightsail", "aws_nlb": "AWS - 网络负载均衡器 NLB", "aws_route53": "AWS - Route53", + "axisnow": "AxisNow", "azure": "Azure(Microsoft Azure)", "azure_dns": "Azure - DNS", "azure_keyvault": "Azure - KeyVault",