diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go
index 9d296065..8ae0014a 100644
--- a/internal/deployer/deployer.go
+++ b/internal/deployer/deployer.go
@@ -21,6 +21,7 @@ const (
targetTencentCDN = "tencent-cdn"
targetTencentCOS = "tencent-cos"
targetHuaweiCloudCDN = "huaweicloud-cdn"
+ targetHuaweiCloudELB = "huaweicloud-elb"
targetQiniuCdn = "qiniu-cdn"
targetLocal = "local"
targetSSH = "ssh"
@@ -110,6 +111,8 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep
return NewTencentCOSDeployer(option)
case targetHuaweiCloudCDN:
return NewHuaweiCloudCDNDeployer(option)
+ case targetHuaweiCloudELB:
+ return NewHuaweiCloudELBDeployer(option)
case targetQiniuCdn:
return NewQiniuCDNDeployer(option)
case targetLocal:
diff --git a/internal/deployer/huaweicloud_cdn.go b/internal/deployer/huaweicloud_cdn.go
index bf87fb89..f7835dcb 100644
--- a/internal/deployer/huaweicloud_cdn.go
+++ b/internal/deployer/huaweicloud_cdn.go
@@ -31,9 +31,9 @@ func NewHuaweiCloudCDNDeployer(option *DeployerOption) (Deployer, error) {
}
client, err := (&HuaweiCloudCDNDeployer{}).createSdkClient(
- option.DeployConfig.GetConfigAsString("region"),
access.AccessKeyId,
access.SecretAccessKey,
+ option.DeployConfig.GetConfigAsString("region"),
)
if err != nil {
return nil, err
@@ -119,7 +119,7 @@ func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error {
return nil
}
-func (d *HuaweiCloudCDNDeployer) createSdkClient(region, accessKeyId, secretAccessKey string) (*hcCdn.CdnClient, error) {
+func (d *HuaweiCloudCDNDeployer) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcCdn.CdnClient, error) {
if region == "" {
region = "cn-north-1" // CDN 服务默认区域:华北一北京
}
diff --git a/internal/deployer/huaweicloud_elb.go b/internal/deployer/huaweicloud_elb.go
new file mode 100644
index 00000000..e9a6f243
--- /dev/null
+++ b/internal/deployer/huaweicloud_elb.go
@@ -0,0 +1,365 @@
+package deployer
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "sort"
+ "strings"
+
+ "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic"
+ "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global"
+ hcElb "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3"
+ hcElbModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/model"
+ hcElbRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/region"
+ hcIam "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3"
+ hcIamModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model"
+ hcIamRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region"
+
+ "github.com/usual2970/certimate/internal/domain"
+ "github.com/usual2970/certimate/internal/pkg/core/uploader"
+ "github.com/usual2970/certimate/internal/pkg/utils/cast"
+)
+
+type HuaweiCloudELBDeployer struct {
+ option *DeployerOption
+ infos []string
+
+ sdkClient *hcElb.ElbClient
+ sslUploader uploader.Uploader
+}
+
+func NewHuaweiCloudELBDeployer(option *DeployerOption) (Deployer, error) {
+ access := &domain.HuaweiCloudAccess{}
+ if err := json.Unmarshal([]byte(option.Access), access); err != nil {
+ return nil, err
+ }
+
+ client, err := (&HuaweiCloudELBDeployer{}).createSdkClient(
+ access.AccessKeyId,
+ access.SecretAccessKey,
+ option.DeployConfig.GetConfigAsString("region"),
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ uploader, err := uploader.NewHuaweiCloudELBUploader(&uploader.HuaweiCloudELBUploaderConfig{
+ Region: option.DeployConfig.GetConfigAsString("region"),
+ AccessKeyId: access.AccessKeyId,
+ SecretAccessKey: access.SecretAccessKey,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return &HuaweiCloudELBDeployer{
+ option: option,
+ infos: make([]string, 0),
+ sdkClient: client,
+ sslUploader: uploader,
+ }, nil
+}
+
+func (d *HuaweiCloudELBDeployer) GetID() string {
+ return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
+}
+
+func (d *HuaweiCloudELBDeployer) GetInfo() []string {
+ return d.infos
+}
+
+func (d *HuaweiCloudELBDeployer) Deploy(ctx context.Context) error {
+ switch d.option.DeployConfig.GetConfigAsString("resourceType") {
+ case "certificate":
+ if err := d.deployToCertificate(ctx); err != nil {
+ return err
+ }
+ case "loadbalancer":
+ if err := d.deployToLoadbalancer(ctx); err != nil {
+ return err
+ }
+ case "listener":
+ if err := d.deployToListener(ctx); err != nil {
+ return err
+ }
+ default:
+ return errors.New("unsupported resource type")
+ }
+
+ return nil
+}
+
+func (d *HuaweiCloudELBDeployer) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcElb.ElbClient, error) {
+ if region == "" {
+ region = "cn-north-4" // ELB 服务默认区域:华北四北京
+ }
+
+ projectId, err := (&HuaweiCloudELBDeployer{}).getSdkProjectId(
+ accessKeyId,
+ secretAccessKey,
+ region,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ auth, err := basic.NewCredentialsBuilder().
+ WithAk(accessKeyId).
+ WithSk(secretAccessKey).
+ WithProjectId(projectId).
+ SafeBuild()
+ if err != nil {
+ return nil, err
+ }
+
+ hcRegion, err := hcElbRegion.SafeValueOf(region)
+ if err != nil {
+ return nil, err
+ }
+
+ hcClient, err := hcElb.ElbClientBuilder().
+ WithRegion(hcRegion).
+ WithCredential(auth).
+ SafeBuild()
+ if err != nil {
+ return nil, err
+ }
+
+ client := hcElb.NewElbClient(hcClient)
+ return client, nil
+}
+
+func (u *HuaweiCloudELBDeployer) getSdkProjectId(accessKeyId, secretAccessKey, region string) (string, error) {
+ if region == "" {
+ region = "cn-north-4" // IAM 服务默认区域:华北四北京
+ }
+
+ auth, err := global.NewCredentialsBuilder().
+ WithAk(accessKeyId).
+ WithSk(secretAccessKey).
+ SafeBuild()
+ if err != nil {
+ return "", err
+ }
+
+ hcRegion, err := hcIamRegion.SafeValueOf(region)
+ if err != nil {
+ return "", err
+ }
+
+ hcClient, err := hcIam.IamClientBuilder().
+ WithRegion(hcRegion).
+ WithCredential(auth).
+ SafeBuild()
+ if err != nil {
+ return "", err
+ }
+
+ client := hcIam.NewIamClient(hcClient)
+ if err != nil {
+ return "", err
+ }
+
+ request := &hcIamModel.KeystoneListProjectsRequest{
+ Name: ®ion,
+ }
+ response, err := client.KeystoneListProjects(request)
+ if err != nil {
+ return "", err
+ } else if response.Projects == nil || len(*response.Projects) == 0 {
+ return "", fmt.Errorf("no project found")
+ }
+
+ return (*response.Projects)[0].Id, nil
+}
+
+func (d *HuaweiCloudELBDeployer) deployToCertificate(ctx context.Context) error {
+ // 更新证书
+ // REF: https://support.huaweicloud.com/api-elb/UpdateCertificate.html
+ updateCertificateReq := &hcElbModel.UpdateCertificateRequest{
+ CertificateId: d.option.DeployConfig.GetConfigAsString("certificateId"),
+ Body: &hcElbModel.UpdateCertificateRequestBody{
+ Certificate: &hcElbModel.UpdateCertificateOption{
+ Certificate: cast.StringPtr(d.option.Certificate.Certificate),
+ PrivateKey: cast.StringPtr(d.option.Certificate.PrivateKey),
+ },
+ },
+ }
+ updateCertificateResp, err := d.sdkClient.UpdateCertificate(updateCertificateReq)
+ if err != nil {
+ return fmt.Errorf("failed to execute sdk request 'elb.UpdateCertificate': %w", err)
+ }
+
+ d.infos = append(d.infos, toStr("已更新 ELB 证书", updateCertificateResp))
+
+ return nil
+}
+
+func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error {
+ // 查询负载均衡器详情
+ // REF: https://support.huaweicloud.com/api-elb/ShowLoadBalancer.html
+ showLoadBalancerReq := &hcElbModel.ShowLoadBalancerRequest{
+ LoadbalancerId: d.option.DeployConfig.GetConfigAsString("loadbalancerId"),
+ }
+ showLoadBalancerResp, err := d.sdkClient.ShowLoadBalancer(showLoadBalancerReq)
+ if err != nil {
+ return fmt.Errorf("failed to execute sdk request 'elb.ShowLoadBalancer': %w", err)
+ }
+
+ d.infos = append(d.infos, toStr("已查询到到 ELB 负载均衡器", showLoadBalancerResp))
+
+ // 查询监听器列表
+ // REF: https://support.huaweicloud.com/api-elb/ListListeners.html
+ listenerIds := make([]string, 0)
+ listListenersLimit := int32(2000)
+ var listListenersMarker *string = nil
+ for {
+ listListenersReq := &hcElbModel.ListListenersRequest{
+ Limit: cast.Int32Ptr(listListenersLimit),
+ Marker: listListenersMarker,
+ Protocol: &[]string{"HTTPS", "TERMINATED_HTTPS"},
+ LoadbalancerId: &[]string{showLoadBalancerResp.Loadbalancer.Id},
+ }
+ listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
+ if err != nil {
+ return fmt.Errorf("failed to execute sdk request 'elb.ListListeners': %w", err)
+ }
+
+ if listListenersResp.Listeners != nil {
+ for _, listener := range *listListenersResp.Listeners {
+ listenerIds = append(listenerIds, listener.Id)
+ }
+ }
+
+ if listListenersResp.Listeners == nil || len(*listListenersResp.Listeners) < int(listListenersLimit) {
+ break
+ } else {
+ listListenersMarker = listListenersResp.PageInfo.NextMarker
+ }
+ }
+
+ d.infos = append(d.infos, toStr("已查询到到 ELB 负载均衡器下的监听器", listenerIds))
+
+ // 上传证书到 SCM
+ uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
+ if err != nil {
+ return err
+ }
+
+ d.infos = append(d.infos, toStr("已上传证书", uploadResult))
+
+ // 批量更新监听器证书
+ var errs []error
+ for _, listenerId := range listenerIds {
+ if err := d.updateListenerCertificate(ctx, listenerId, uploadResult.CertId); err != nil {
+ errs = append(errs, err)
+ }
+ }
+ if len(errs) > 0 {
+ return errors.Join(errs...)
+ }
+
+ return nil
+}
+
+func (d *HuaweiCloudELBDeployer) deployToListener(ctx context.Context) error {
+ // 上传证书到 SCM
+ uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
+ if err != nil {
+ return err
+ }
+
+ d.infos = append(d.infos, toStr("已上传证书", uploadResult))
+
+ // 更新监听器证书
+ if err := d.updateListenerCertificate(ctx, d.option.DeployConfig.GetConfigAsString("listenerId"), uploadResult.CertId); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (d *HuaweiCloudELBDeployer) updateListenerCertificate(ctx context.Context, hcListenerId string, hcCertId string) error {
+ // 查询监听器详情
+ // REF: https://support.huaweicloud.com/api-elb/ShowListener.html
+ showListenerReq := &hcElbModel.ShowListenerRequest{
+ ListenerId: hcListenerId,
+ }
+ showListenerResp, err := d.sdkClient.ShowListener(showListenerReq)
+ if err != nil {
+ return fmt.Errorf("failed to execute sdk request 'elb.ShowListener': %w", err)
+ }
+
+ d.infos = append(d.infos, toStr("已查询到到 ELB 监听器", showListenerResp))
+
+ // 更新监听器
+ // REF: https://support.huaweicloud.com/api-elb/UpdateListener.html
+ updateListenerReq := &hcElbModel.UpdateListenerRequest{
+ ListenerId: hcListenerId,
+ Body: &hcElbModel.UpdateListenerRequestBody{
+ Listener: &hcElbModel.UpdateListenerOption{
+ DefaultTlsContainerRef: cast.StringPtr(hcCertId),
+ },
+ },
+ }
+ if showListenerResp.Listener.SniContainerRefs != nil {
+ if len(showListenerResp.Listener.SniContainerRefs) > 0 {
+ // 如果开启 SNI,需替换同 SAN 的证书
+ sniCertIds := make([]string, 0)
+ sniCertIds = append(sniCertIds, hcCertId)
+
+ listOldCertificateReq := &hcElbModel.ListCertificatesRequest{
+ Id: &showListenerResp.Listener.SniContainerRefs,
+ }
+ listOldCertificateResp, err := d.sdkClient.ListCertificates(listOldCertificateReq)
+ if err != nil {
+ return fmt.Errorf("failed to execute sdk request 'elb.ListCertificates': %w", err)
+ }
+
+ showNewCertificateReq := &hcElbModel.ShowCertificateRequest{
+ CertificateId: hcCertId,
+ }
+ showNewCertificateResp, err := d.sdkClient.ShowCertificate(showNewCertificateReq)
+ if err != nil {
+ return fmt.Errorf("failed to execute sdk request 'elb.ShowCertificate': %w", err)
+ }
+
+ for _, certificate := range *listOldCertificateResp.Certificates {
+ oldCertificate := certificate
+ newCertificate := showNewCertificateResp.Certificate
+
+ if oldCertificate.SubjectAlternativeNames != nil && newCertificate.SubjectAlternativeNames != nil {
+ oldCertificateSans := oldCertificate.SubjectAlternativeNames
+ newCertificateSans := newCertificate.SubjectAlternativeNames
+ sort.Strings(*oldCertificateSans)
+ sort.Strings(*newCertificateSans)
+ if strings.Join(*oldCertificateSans, ";") == strings.Join(*newCertificateSans, ";") {
+ continue
+ }
+ } else {
+ if oldCertificate.Domain == newCertificate.Domain {
+ continue
+ }
+ }
+
+ sniCertIds = append(sniCertIds, certificate.Id)
+ }
+
+ updateListenerReq.Body.Listener.SniContainerRefs = &sniCertIds
+ }
+
+ if showListenerResp.Listener.SniMatchAlgo != "" {
+ updateListenerReq.Body.Listener.SniMatchAlgo = cast.StringPtr(showListenerResp.Listener.SniMatchAlgo)
+ }
+ }
+ updateListenerResp, err := d.sdkClient.UpdateListener(updateListenerReq)
+ if err != nil {
+ return fmt.Errorf("failed to execute sdk request 'elb.UpdateListener': %w", err)
+ }
+
+ d.infos = append(d.infos, toStr("已更新监听器", updateListenerResp))
+
+ return nil
+}
diff --git a/internal/pkg/core/uploader/uploader_huaweicloud_elb.go b/internal/pkg/core/uploader/uploader_huaweicloud_elb.go
index 5eb60d88..090362af 100644
--- a/internal/pkg/core/uploader/uploader_huaweicloud_elb.go
+++ b/internal/pkg/core/uploader/uploader_huaweicloud_elb.go
@@ -6,19 +6,22 @@ import (
"time"
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic"
+ "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global"
hcElb "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3"
hcElbModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/model"
hcElbRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/region"
+ hcIam "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3"
+ hcIamModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model"
+ hcIamRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
type HuaweiCloudELBUploaderConfig struct {
- Region string `json:"region"`
- ProjectId string `json:"projectId"`
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
+ Region string `json:"region"`
}
type HuaweiCloudELBUploader struct {
@@ -28,9 +31,9 @@ type HuaweiCloudELBUploader struct {
func NewHuaweiCloudELBUploader(config *HuaweiCloudELBUploaderConfig) (Uploader, error) {
client, err := (&HuaweiCloudELBUploader{}).createSdkClient(
- config.Region,
config.AccessKeyId,
config.SecretAccessKey,
+ config.Region,
)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
@@ -100,6 +103,13 @@ func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, pri
}
}
+ // 获取项目 ID
+ // REF: https://support.huaweicloud.com/api-iam/iam_06_0001.html
+ projectId, err := u.getSdkProjectId(u.config.Region, u.config.AccessKeyId, u.config.SecretAccessKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get SDK project id: %w", err)
+ }
+
// 生成新证书名(需符合华为云命名规则)
var certId, certName string
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
@@ -109,7 +119,7 @@ func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, pri
createCertificateReq := &hcElbModel.CreateCertificateRequest{
Body: &hcElbModel.CreateCertificateRequestBody{
Certificate: &hcElbModel.CreateCertificateOption{
- ProjectId: cast.StringPtr(u.config.ProjectId),
+ ProjectId: cast.StringPtr(projectId),
Name: cast.StringPtr(certName),
Certificate: cast.StringPtr(certPem),
PrivateKey: cast.StringPtr(privkeyPem),
@@ -129,7 +139,7 @@ func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, pri
}, nil
}
-func (u *HuaweiCloudELBUploader) createSdkClient(region, accessKeyId, secretAccessKey string) (*hcElb.ElbClient, error) {
+func (u *HuaweiCloudELBUploader) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcElb.ElbClient, error) {
if region == "" {
region = "cn-north-4" // ELB 服务默认区域:华北四北京
}
@@ -158,3 +168,47 @@ func (u *HuaweiCloudELBUploader) createSdkClient(region, accessKeyId, secretAcce
client := hcElb.NewElbClient(hcClient)
return client, nil
}
+
+func (u *HuaweiCloudELBUploader) getSdkProjectId(accessKeyId, secretAccessKey, region string) (string, error) {
+ if region == "" {
+ region = "cn-north-4" // IAM 服务默认区域:华北四北京
+ }
+
+ auth, err := global.NewCredentialsBuilder().
+ WithAk(accessKeyId).
+ WithSk(secretAccessKey).
+ SafeBuild()
+ if err != nil {
+ return "", err
+ }
+
+ hcRegion, err := hcIamRegion.SafeValueOf(region)
+ if err != nil {
+ return "", err
+ }
+
+ hcClient, err := hcIam.IamClientBuilder().
+ WithRegion(hcRegion).
+ WithCredential(auth).
+ SafeBuild()
+ if err != nil {
+ return "", err
+ }
+
+ client := hcIam.NewIamClient(hcClient)
+ if err != nil {
+ return "", err
+ }
+
+ request := &hcIamModel.KeystoneListProjectsRequest{
+ Name: ®ion,
+ }
+ response, err := client.KeystoneListProjects(request)
+ if err != nil {
+ return "", err
+ } else if response.Projects == nil || len(*response.Projects) == 0 {
+ return "", fmt.Errorf("no project found")
+ }
+
+ return (*response.Projects)[0].Id, nil
+}
diff --git a/internal/pkg/core/uploader/uploader_huaweicloud_scm.go b/internal/pkg/core/uploader/uploader_huaweicloud_scm.go
index 30864a48..2b09ca19 100644
--- a/internal/pkg/core/uploader/uploader_huaweicloud_scm.go
+++ b/internal/pkg/core/uploader/uploader_huaweicloud_scm.go
@@ -15,9 +15,9 @@ import (
)
type HuaweiCloudSCMUploaderConfig struct {
- Region string `json:"region"`
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
+ Region string `json:"region"`
}
type HuaweiCloudSCMUploader struct {
@@ -27,9 +27,9 @@ type HuaweiCloudSCMUploader struct {
func NewHuaweiCloudSCMUploader(config *HuaweiCloudSCMUploaderConfig) (Uploader, error) {
client, err := (&HuaweiCloudSCMUploader{}).createSdkClient(
- config.Region,
config.AccessKeyId,
config.SecretAccessKey,
+ config.Region,
)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
@@ -137,7 +137,7 @@ func (u *HuaweiCloudSCMUploader) Upload(ctx context.Context, certPem string, pri
}, nil
}
-func (u *HuaweiCloudSCMUploader) createSdkClient(region, accessKeyId, secretAccessKey string) (*hcScm.ScmClient, error) {
+func (u *HuaweiCloudSCMUploader) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcScm.ScmClient, error) {
if region == "" {
region = "cn-north-4" // SCM 服务默认区域:华北四北京
}
diff --git a/ui/src/components/certimate/DeployEditDialog.tsx b/ui/src/components/certimate/DeployEditDialog.tsx
index 54710e49..974dc590 100644
--- a/ui/src/components/certimate/DeployEditDialog.tsx
+++ b/ui/src/components/certimate/DeployEditDialog.tsx
@@ -14,6 +14,7 @@ import DeployToAliyunCDN from "./DeployToAliyunCDN";
import DeployToTencentCDN from "./DeployToTencentCDN";
import DeployToTencentCOS from "./DeployToTencentCOS";
import DeployToHuaweiCloudCDN from "./DeployToHuaweiCloudCDN";
+import DeployToHuaweiCloudELB from "./DeployToHuaweiCloudELB";
import DeployToQiniuCDN from "./DeployToQiniuCDN";
import DeployToSSH from "./DeployToSSH";
import DeployToWebhook from "./DeployToWebhook";
@@ -82,7 +83,7 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro
return true;
}
- return item.configType === locDeployConfig.type.split("-")[0];
+ return item.configType === deployTargetsMap.get(locDeployConfig.type)?.provider;
});
const handleSaveClick = () => {
@@ -125,6 +126,9 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro
case "huaweicloud-cdn":
childComponent =