diff --git a/.changelog/32482.txt b/.changelog/32482.txt new file mode 100644 index 000000000000..834c75c2ec08 --- /dev/null +++ b/.changelog/32482.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_workspaces_connection_alias +``` \ No newline at end of file diff --git a/GNUmakefile b/GNUmakefile index 91e836a521c7..fb640f22def7 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -189,6 +189,8 @@ importlint: lint: golangci-lint providerlint importlint +lint-fix: testacc-lint-fix website-lint-fix docs-lint-fix + providerlint: @echo "==> Checking source code with providerlint..." @providerlint \ @@ -429,6 +431,7 @@ yamllint: golangci-lint \ importlint \ lint \ + lint-fix \ providerlint \ sane \ sanity \ diff --git a/internal/service/workspaces/connection_alias.go b/internal/service/workspaces/connection_alias.go new file mode 100644 index 000000000000..33d374f4e273 --- /dev/null +++ b/internal/service/workspaces/connection_alias.go @@ -0,0 +1,313 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package workspaces + +import ( + "context" + "errors" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/workspaces" + awstypes "github.com/aws/aws-sdk-go-v2/service/workspaces/types" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/enum" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +var ResourceConnectionAlias = newResourceConnectionAlias + +// @FrameworkResource(name="Connection Alias") +// @Tags(identifierAttribute="id") +func newResourceConnectionAlias(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &resourceConnectionAlias{} + + r.SetDefaultCreateTimeout(30 * time.Minute) + r.SetDefaultUpdateTimeout(30 * time.Minute) + r.SetDefaultDeleteTimeout(30 * time.Minute) + + return r, nil +} + +const ( + ResNameConnectionAlias = "Connection Alias" +) + +type resourceConnectionAlias struct { + framework.ResourceWithConfigure + framework.WithTimeouts +} + +func (r *resourceConnectionAlias) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "aws_workspaces_connection_alias" +} + +func (r *resourceConnectionAlias) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": framework.IDAttribute(), + "connection_string": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Description: "The connection string specified for the connection alias. The connection string must be in the form of a fully qualified domain name (FQDN), such as www.example.com.", + }, + "owner_account_id": schema.StringAttribute{ + Computed: true, + Description: "The identifier of the Amazon Web Services account that owns the connection alias.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "state": schema.StringAttribute{ + Computed: true, + Description: "The current state of the connection alias.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + names.AttrTags: tftags.TagsAttribute(), + names.AttrTagsAll: tftags.TagsAttributeComputedOnly(), + }, + Blocks: map[string]schema.Block{ + "timeouts": timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Update: true, + Delete: true, + }), + }, + } +} + +func (r *resourceConnectionAlias) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + conn := r.Meta().WorkSpacesClient(ctx) + + var plan resourceConnectionAliasData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + in := &workspaces.CreateConnectionAliasInput{ + ConnectionString: aws.String(plan.ConnectionString.ValueString()), + Tags: getTagsIn(ctx), + } + + out, err := conn.CreateConnectionAlias(ctx, in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.WorkSpaces, create.ErrActionCreating, ResNameConnectionAlias, plan.ConnectionString.String(), err), + err.Error(), + ) + return + } + if out == nil || out.AliasId == nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.WorkSpaces, create.ErrActionCreating, ResNameConnectionAlias, plan.ConnectionString.String(), nil), + errors.New("empty output").Error(), + ) + return + } + + plan.ID = flex.StringToFramework(ctx, out.AliasId) + + createTimeout := r.CreateTimeout(ctx, plan.Timeouts) + alias, err := waitConnectionAliasCreated(ctx, conn, plan.ID.ValueString(), createTimeout) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.WorkSpaces, create.ErrActionWaitingForCreation, ResNameConnectionAlias, plan.ID.String(), err), + err.Error(), + ) + return + } + + plan.update(ctx, alias) + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *resourceConnectionAlias) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().WorkSpacesClient(ctx) + + var state resourceConnectionAliasData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + out, err := FindConnectionAliasByID(ctx, conn, state.ID.ValueString()) + if tfresource.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.WorkSpaces, create.ErrActionSetting, ResNameConnectionAlias, state.ID.String(), err), + err.Error(), + ) + return + } + + state.update(ctx, out) + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceConnectionAlias) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state resourceConnectionAliasData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *resourceConnectionAlias) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn := r.Meta().WorkSpacesClient(ctx) + + var state resourceConnectionAliasData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + in := &workspaces.DeleteConnectionAliasInput{ + AliasId: aws.String(state.ID.ValueString()), + } + + _, err := conn.DeleteConnectionAlias(ctx, in) + if err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.WorkSpaces, create.ErrActionDeleting, ResNameConnectionAlias, state.ID.String(), err), + err.Error(), + ) + return + } + + deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts) + _, err = waitConnectionAliasDeleted(ctx, conn, state.ID.ValueString(), deleteTimeout) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.WorkSpaces, create.ErrActionWaitingForDeletion, ResNameConnectionAlias, state.ID.String(), err), + err.Error(), + ) + return + } +} + +func (r *resourceConnectionAlias) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *resourceConnectionAlias) ModifyPlan(ctx context.Context, request resource.ModifyPlanRequest, response *resource.ModifyPlanResponse) { + r.SetTagsAll(ctx, request, response) +} + +func (data *resourceConnectionAliasData) update(ctx context.Context, in *awstypes.ConnectionAlias) { + data.ConnectionString = flex.StringToFramework(ctx, in.ConnectionString) + data.OwnerAccountId = flex.StringToFramework(ctx, in.OwnerAccountId) + data.State = flex.StringValueToFramework(ctx, in.State) +} + +func waitConnectionAliasCreated(ctx context.Context, conn *workspaces.Client, id string, timeout time.Duration) (*awstypes.ConnectionAlias, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.ConnectionAliasStateCreating), + Target: enum.Slice(awstypes.ConnectionAliasStateCreated), + Refresh: statusConnectionAlias(ctx, conn, id), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*awstypes.ConnectionAlias); ok { + return out, err + } + + return nil, err +} + +func waitConnectionAliasDeleted(ctx context.Context, conn *workspaces.Client, id string, timeout time.Duration) (*awstypes.ConnectionAlias, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.ConnectionAliasStateDeleting), + Target: []string{}, + Refresh: statusConnectionAlias(ctx, conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*awstypes.ConnectionAlias); ok { + return out, err + } + + return nil, err +} + +func statusConnectionAlias(ctx context.Context, conn *workspaces.Client, id string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + out, err := FindConnectionAliasByID(ctx, conn, id) + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return out, string(out.State), nil + } +} + +func FindConnectionAliasByID(ctx context.Context, conn *workspaces.Client, id string) (*awstypes.ConnectionAlias, error) { + in := &workspaces.DescribeConnectionAliasesInput{ + AliasIds: []string{id}, + } + + out, err := conn.DescribeConnectionAliases(ctx, in) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + + if err != nil { + return nil, err + } + + if out == nil || len(out.ConnectionAliases) == 0 { + return nil, tfresource.NewEmptyResultError(in) + } + + return &out.ConnectionAliases[0], nil +} + +type resourceConnectionAliasData struct { + ID types.String `tfsdk:"id"` + ConnectionString types.String `tfsdk:"connection_string"` + OwnerAccountId types.String `tfsdk:"owner_account_id"` + State types.String `tfsdk:"state"` + Tags types.Map `tfsdk:"tags"` + TagsAll types.Map `tfsdk:"tags_all"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} diff --git a/internal/service/workspaces/connection_alias_test.go b/internal/service/workspaces/connection_alias_test.go new file mode 100644 index 000000000000..0ee49b5cca98 --- /dev/null +++ b/internal/service/workspaces/connection_alias_test.go @@ -0,0 +1,235 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package workspaces_test + +import ( + "context" + "errors" + "fmt" + "strings" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/workspaces" + awstypes "github.com/aws/aws-sdk-go-v2/service/workspaces/types" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/create" + tfworkspaces "github.com/hashicorp/terraform-provider-aws/internal/service/workspaces" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccWorkSpacesConnectionAlias_basic(t *testing.T) { + ctx := acctest.Context(t) + + var connectionalias awstypes.ConnectionAlias + rName := acctest.RandomFQDomainName() + resourceName := "aws_workspaces_connection_alias.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, strings.ToLower(workspaces.ServiceID)) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, strings.ToLower(workspaces.ServiceID)), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckConnectionAliasDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccConnectionAliasConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckConnectionAliasExists(ctx, resourceName, &connectionalias), + resource.TestCheckResourceAttr(resourceName, "connection_string", rName), + resource.TestCheckResourceAttrSet(resourceName, "owner_account_id"), + resource.TestCheckResourceAttrSet(resourceName, "state"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccWorkSpacesConnectionAlias_disappears(t *testing.T) { + ctx := acctest.Context(t) + + var connectionalias awstypes.ConnectionAlias + rName := acctest.RandomFQDomainName() + resourceName := "aws_workspaces_connection_alias.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, strings.ToLower(workspaces.ServiceID)) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, strings.ToLower(workspaces.ServiceID)), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckConnectionAliasDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccConnectionAliasConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckConnectionAliasExists(ctx, resourceName, &connectionalias), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfworkspaces.ResourceConnectionAlias, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccWorkSpacesConnectionAlias_tags(t *testing.T) { + ctx := acctest.Context(t) + + var connectionalias awstypes.ConnectionAlias + rName := acctest.RandomFQDomainName() + resourceName := "aws_workspaces_connection_alias.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, strings.ToLower(workspaces.ServiceID)) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, strings.ToLower(workspaces.ServiceID)), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckConnectionAliasDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccConnectionAliasConfig_tags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckConnectionAliasExists(ctx, resourceName, &connectionalias), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccConnectionAliasConfig_tags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckConnectionAliasExists(ctx, resourceName, &connectionalias), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccConnectionAliasConfig_tags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckConnectionAliasExists(ctx, resourceName, &connectionalias), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func testAccCheckConnectionAliasDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).WorkSpacesClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_workspaces_connection_alias" { + continue + } + + _, err := tfworkspaces.FindConnectionAliasByID(ctx, conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + return nil + } + + if err != nil { + return err + } + + return create.Error(names.WorkSpaces, create.ErrActionCheckingDestroyed, tfworkspaces.ResNameConnectionAlias, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccCheckConnectionAliasExists(ctx context.Context, name string, connectionalias *awstypes.ConnectionAlias) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.WorkSpaces, create.ErrActionCheckingExistence, tfworkspaces.ResNameConnectionAlias, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.WorkSpaces, create.ErrActionCheckingExistence, tfworkspaces.ResNameConnectionAlias, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).WorkSpacesClient(ctx) + out, err := tfworkspaces.FindConnectionAliasByID(ctx, conn, rs.Primary.ID) + + if err != nil { + return create.Error(names.WorkSpaces, create.ErrActionCheckingExistence, tfworkspaces.ResNameConnectionAlias, rs.Primary.ID, err) + } + + *connectionalias = *out + + return nil + } +} + +func testAccPreCheck(ctx context.Context, t *testing.T) { + conn := acctest.Provider.Meta().(*conns.AWSClient).WorkSpacesClient(ctx) + + input := &workspaces.DescribeConnectionAliasesInput{} + _, err := conn.DescribeConnectionAliases(ctx, input) + + if acctest.PreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccConnectionAliasConfig_basic(rName string) string { + return fmt.Sprintf(` +resource "aws_workspaces_connection_alias" "test" { + connection_string = %[1]q +} +`, rName) +} + +func testAccConnectionAliasConfig_tags1(rName, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_workspaces_connection_alias" "test" { + connection_string = %[1]q + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1) +} + +func testAccConnectionAliasConfig_tags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_workspaces_connection_alias" "test" { + connection_string = %[1]q + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} diff --git a/internal/service/workspaces/service_package_gen.go b/internal/service/workspaces/service_package_gen.go index 46a3af1b4616..9854cc6c9d81 100644 --- a/internal/service/workspaces/service_package_gen.go +++ b/internal/service/workspaces/service_package_gen.go @@ -19,7 +19,15 @@ func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*types.Serv } func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.ServicePackageFrameworkResource { - return []*types.ServicePackageFrameworkResource{} + return []*types.ServicePackageFrameworkResource{ + { + Factory: newResourceConnectionAlias, + Name: "Connection Alias", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: "id", + }, + }, + } } func (p *servicePackage) SDKDataSources(ctx context.Context) []*types.ServicePackageSDKDataSource { diff --git a/website/docs/r/workspaces_connection_alias.html.markdown b/website/docs/r/workspaces_connection_alias.html.markdown new file mode 100644 index 000000000000..54a936eba428 --- /dev/null +++ b/website/docs/r/workspaces_connection_alias.html.markdown @@ -0,0 +1,53 @@ +--- +subcategory: "WorkSpaces" +layout: "aws" +page_title: "AWS: aws_workspaces_connection_alias" +description: |- + Terraform resource for managing an AWS WorkSpaces Connection Alias. +--- + +# Resource: aws_workspaces_connection_alias + +Terraform resource for managing an AWS WorkSpaces Connection Alias. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_workspaces_connection_alias" "example" { + connection_string = "testdomain.test" +} +``` + +## Argument Reference + +The following arguments are required: + +* `connection_string` - (Required) The connection string specified for the connection alias. The connection string must be in the form of a fully qualified domain name (FQDN), such as www.example.com. +* `tags` – (Optional) A map of tags assigned to the WorkSpaces Connection Alias. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `id` - The identifier of the connection alias. +* `owner_account_id` - The identifier of the Amazon Web Services account that owns the connection alias. +* `state` - The current state of the connection alias. +* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block). + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `60m`) +* `update` - (Default `180m`) +* `delete` - (Default `90m`) + +## Import + +Import WorkSpaces Connection Alias using the connection alias ID. For example: + +``` +$ terraform import aws_workspaces_connection_alias.example rft-8012925589 +```