diff --git a/.changelog/8123.txt b/.changelog/8123.txt new file mode 100644 index 00000000000..187897cf666 --- /dev/null +++ b/.changelog/8123.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_codepipeline_custom_action_type +``` \ No newline at end of file diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 795aada5271..739f41aae23 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1172,8 +1172,9 @@ func New(_ context.Context) (*schema.Provider, error) { "aws_codedeploy_deployment_config": deploy.ResourceDeploymentConfig(), "aws_codedeploy_deployment_group": deploy.ResourceDeploymentGroup(), - "aws_codepipeline": codepipeline.ResourceCodePipeline(), - "aws_codepipeline_webhook": codepipeline.ResourceWebhook(), + "aws_codepipeline": codepipeline.ResourcePipeline(), + "aws_codepipeline_custom_action_type": codepipeline.ResourceCustomActionType(), + "aws_codepipeline_webhook": codepipeline.ResourceWebhook(), "aws_codestarconnections_connection": codestarconnections.ResourceConnection(), "aws_codestarconnections_host": codestarconnections.ResourceHost(), diff --git a/internal/service/codepipeline/codepipeline.go b/internal/service/codepipeline/codepipeline.go index 7c4abc205b5..1298201a261 100644 --- a/internal/service/codepipeline/codepipeline.go +++ b/internal/service/codepipeline/codepipeline.go @@ -1,10 +1,12 @@ package codepipeline import ( + "context" "crypto/sha256" "encoding/hex" "errors" "fmt" + "log" "regexp" "strings" @@ -17,12 +19,10 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-aws/internal/conns" - "github.com/hashicorp/terraform-provider-aws/internal/create" "github.com/hashicorp/terraform-provider-aws/internal/flex" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/internal/verify" - "github.com/hashicorp/terraform-provider-aws/names" ) const ( @@ -31,12 +31,13 @@ const ( gitHubActionConfigurationOAuthToken = "OAuthToken" ) -func ResourceCodePipeline() *schema.Resource { // nosemgrep:ci.codepipeline-in-func-name +func ResourcePipeline() *schema.Resource { return &schema.Resource{ - Create: resourceCreate, - Read: resourceRead, - Update: resourceUpdate, - Delete: resourceDelete, + CreateWithoutTimeout: resourcePipelineCreate, + ReadWithoutTimeout: resourcePipelineRead, + UpdateWithoutTimeout: resourcePipelineUpdate, + DeleteWithoutTimeout: resourcePipelineDelete, + Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, }, @@ -46,36 +47,11 @@ func ResourceCodePipeline() *schema.Resource { // nosemgrep:ci.codepipeline-in-f Type: schema.TypeString, Computed: true, }, - - "name": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validation.All( - validation.StringLenBetween(1, 100), - validation.StringMatch(regexp.MustCompile(`[A-Za-z0-9.@\-_]+`), ""), - ), - }, - - "role_arn": { - Type: schema.TypeString, - Required: true, - ValidateFunc: verify.ValidARN, - }, "artifact_store": { Type: schema.TypeSet, Required: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "location": { - Type: schema.TypeString, - Required: true, - }, - "type": { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringInSlice(codepipeline.ArtifactStoreType_Values(), false), - }, "encryption_key": { Type: schema.TypeList, MaxItems: 1, @@ -94,33 +70,53 @@ func ResourceCodePipeline() *schema.Resource { // nosemgrep:ci.codepipeline-in-f }, }, }, + "location": { + Type: schema.TypeString, + Required: true, + }, "region": { Type: schema.TypeString, Optional: true, Computed: true, }, + "type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(codepipeline.ArtifactStoreType_Values(), false), + }, }, }, }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 100), + validation.StringMatch(regexp.MustCompile(`[A-Za-z0-9.@\-_]+`), ""), + ), + }, + "role_arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: verify.ValidARN, + }, "stage": { Type: schema.TypeList, MinItems: 2, Required: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.All( - validation.StringLenBetween(1, 100), - validation.StringMatch(regexp.MustCompile(`[A-Za-z0-9.@\-_]+`), ""), - ), - }, "action": { Type: schema.TypeList, Required: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ + "category": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(codepipeline.ActionCategory_Values(), false), + }, "configuration": { Type: schema.TypeMap, Optional: true, @@ -129,48 +125,48 @@ func ResourceCodePipeline() *schema.Resource { // nosemgrep:ci.codepipeline-in-f validation.MapKeyLenBetween(1, 1000), ), Elem: &schema.Schema{Type: schema.TypeString}, - DiffSuppressFunc: suppressStageActionConfiguration, - }, - "category": { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringInSlice(codepipeline.ActionCategory_Values(), false), - }, - "owner": { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringInSlice(codepipeline.ActionOwner_Values(), false), + DiffSuppressFunc: pipelineSuppressStageActionConfigurationDiff, }, - "provider": { - Type: schema.TypeString, - Required: true, - ValidateDiagFunc: resourceValidateActionProvider, + "input_artifacts": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, }, - "version": { + "name": { Type: schema.TypeString, Required: true, ValidateFunc: validation.All( - validation.StringLenBetween(1, 9), - validation.StringMatch(regexp.MustCompile(`[0-9A-Za-z_-]+`), ""), + validation.StringLenBetween(1, 100), + validation.StringMatch(regexp.MustCompile(`[A-Za-z0-9.@\-_]+`), ""), ), }, - "input_artifacts": { - Type: schema.TypeList, + "namespace": { + Type: schema.TypeString, Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 100), + validation.StringMatch(regexp.MustCompile(`[A-Za-z0-9@\-_]+`), ""), + ), }, "output_artifacts": { Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, }, - "name": { + "owner": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(codepipeline.ActionOwner_Values(), false), + }, + "provider": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: pipelineValidateActionProvider, + }, + "region": { Type: schema.TypeString, - Required: true, - ValidateFunc: validation.All( - validation.StringLenBetween(1, 100), - validation.StringMatch(regexp.MustCompile(`[A-Za-z0-9.@\-_]+`), ""), - ), + Optional: true, + Computed: true, }, "role_arn": { Type: schema.TypeString, @@ -183,22 +179,25 @@ func ResourceCodePipeline() *schema.Resource { // nosemgrep:ci.codepipeline-in-f Computed: true, ValidateFunc: validation.IntBetween(1, 999), }, - "region": { - Type: schema.TypeString, - Optional: true, - Computed: true, - }, - "namespace": { + "version": { Type: schema.TypeString, - Optional: true, + Required: true, ValidateFunc: validation.All( - validation.StringLenBetween(1, 100), - validation.StringMatch(regexp.MustCompile(`[A-Za-z0-9@\-_]+`), ""), + validation.StringLenBetween(1, 9), + validation.StringMatch(regexp.MustCompile(`[0-9A-Za-z_-]+`), ""), ), }, }, }, }, + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 100), + validation.StringMatch(regexp.MustCompile(`[A-Za-z0-9.@\-_]+`), ""), + ), + }, }, }, }, @@ -210,474 +209,730 @@ func ResourceCodePipeline() *schema.Resource { // nosemgrep:ci.codepipeline-in-f } } -func resourceCreate(d *schema.ResourceData, meta interface{}) error { +func resourcePipelineCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).CodePipelineConn defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) - pipeline, err := expand(d) + pipeline, err := expandPipelineDeclaration(d) + if err != nil { - return err + return diag.FromErr(err) } - params := &codepipeline.CreatePipelineInput{ + + name := d.Get("name").(string) + input := &codepipeline.CreatePipelineInput{ Pipeline: pipeline, - Tags: Tags(tags.IgnoreAWS()), } - var resp *codepipeline.CreatePipelineOutput - err = resource.Retry(propagationTimeout, func() *resource.RetryError { - var err error + if len(tags) > 0 { + input.Tags = Tags(tags.IgnoreAWS()) + } + + outputRaw, err := tfresource.RetryWhenAWSErrMessageContainsContext(ctx, propagationTimeout, func() (interface{}, error) { + return conn.CreatePipelineWithContext(ctx, input) + }, codepipeline.ErrCodeInvalidStructureException, "not authorized") + + if err != nil { + return diag.Errorf("creating CodePipeline (%s): %s", name, err) + } + + d.SetId(aws.StringValue(outputRaw.(*codepipeline.CreatePipelineOutput).Pipeline.Name)) + + return resourcePipelineRead(ctx, d, meta) +} + +func resourcePipelineRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).CodePipelineConn + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig + + output, err := FindPipelineByName(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] CodePipeline %s not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.Errorf("reading CodePipeline (%s): %s", d.Id(), err) + } - resp, err = conn.CreatePipeline(params) + metadata := output.Metadata + pipeline := output.Pipeline - if tfawserr.ErrMessageContains(err, codepipeline.ErrCodeInvalidStructureException, "not authorized") { - return resource.RetryableError(err) + if pipeline.ArtifactStore != nil { + if err := d.Set("artifact_store", []interface{}{flattenArtifactStore(pipeline.ArtifactStore)}); err != nil { + return diag.Errorf("setting artifact_store: %s", err) } + } else if pipeline.ArtifactStores != nil { + if err := d.Set("artifact_store", flattenArtifactStores(pipeline.ArtifactStores)); err != nil { + return diag.Errorf("setting artifact_store: %s", err) + } + } + + if err := d.Set("stage", flattenStageDeclarations(d, pipeline.Stages)); err != nil { + return diag.Errorf("setting stage: %s", err) + } + + arn := aws.StringValue(metadata.PipelineArn) + d.Set("arn", arn) + d.Set("name", pipeline.Name) + d.Set("role_arn", pipeline.RoleArn) + + tags, err := ListTagsWithContext(ctx, conn, arn) + + if err != nil { + return diag.Errorf("listing tags for CodePipeline (%s): %s", arn, err) + } + + tags = tags.IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return diag.Errorf("setting tags: %s", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return diag.Errorf("setting tags_all: %s", err) + } + + return nil +} + +func resourcePipelineUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).CodePipelineConn + + if d.HasChangesExcept("tags", "tags_all") { + pipeline, err := expandPipelineDeclaration(d) if err != nil { - return resource.NonRetryableError(err) + return diag.FromErr(err) } - return nil - }) - if tfresource.TimedOut(err) { - resp, err = conn.CreatePipeline(params) + _, err = conn.UpdatePipelineWithContext(ctx, &codepipeline.UpdatePipelineInput{ + Pipeline: pipeline, + }) + + if err != nil { + return diag.Errorf("updating CodePipeline (%s): %s", d.Id(), err) + } } - if err != nil { - return fmt.Errorf("Error creating CodePipeline: %w", err) + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + arn := d.Get("arn").(string) + + if err := UpdateTagsWithContext(ctx, conn, arn, o, n); err != nil { + return diag.Errorf("updating CodePipeline (%s) tags: %s", arn, err) + } } - if resp.Pipeline == nil { - return fmt.Errorf("Error creating CodePipeline: invalid response from AWS") + + return resourcePipelineRead(ctx, d, meta) +} + +func resourcePipelineDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).CodePipelineConn + + log.Printf("[INFO] Deleting CodePipeline: %s", d.Id()) + _, err := conn.DeletePipelineWithContext(ctx, &codepipeline.DeletePipelineInput{ + Name: aws.String(d.Id()), + }) + + if tfawserr.ErrCodeEquals(err, codepipeline.ErrCodePipelineNotFoundException) { + return nil } - d.SetId(aws.StringValue(resp.Pipeline.Name)) + if err != nil { + return diag.Errorf("deleting CodePipeline (%s): %s", d.Id(), err) + } - return resourceRead(d, meta) + return nil } -func expand(d *schema.ResourceData) (*codepipeline.PipelineDeclaration, error) { - pipeline := codepipeline.PipelineDeclaration{ - Name: aws.String(d.Get("name").(string)), - RoleArn: aws.String(d.Get("role_arn").(string)), - Stages: expandStages(d), +func FindPipelineByName(ctx context.Context, conn *codepipeline.CodePipeline, name string) (*codepipeline.GetPipelineOutput, error) { + input := &codepipeline.GetPipelineInput{ + Name: aws.String(name), + } + + output, err := conn.GetPipelineWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, codepipeline.ErrCodePipelineNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } } - pipelineArtifactStores, err := ExpandArtifactStores(d.Get("artifact_store").(*schema.Set).List()) if err != nil { return nil, err } - if len(pipelineArtifactStores) == 1 { - for _, v := range pipelineArtifactStores { - pipeline.ArtifactStore = v + + if output == nil || output.Metadata == nil || output.Pipeline == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output, nil +} + +func pipelineValidateActionProvider(i interface{}, path cty.Path) diag.Diagnostics { + v, ok := i.(string) + if !ok { + return diag.Errorf("expected type to be string") + } + + if v == providerGitHub { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "The CodePipeline GitHub version 1 action provider is deprecated.", + Detail: "Use a GitHub version 2 action (with a CodeStar Connection `aws_codestarconnections_connection`) instead. See https://docs.aws.amazon.com/codepipeline/latest/userguide/update-github-action-connections.html", + }, } - } else { - pipeline.ArtifactStores = pipelineArtifactStores } - return &pipeline, nil + return nil } -func ExpandArtifactStores(configs []interface{}) (map[string]*codepipeline.ArtifactStore, error) { - if len(configs) == 0 { - return nil, nil +func pipelineSuppressStageActionConfigurationDiff(k, old, new string, d *schema.ResourceData) bool { + parts := strings.Split(k, ".") + parts = parts[:len(parts)-2] + providerAddr := strings.Join(append(parts, "provider"), ".") + provider := d.Get(providerAddr).(string) + + if provider == providerGitHub && strings.HasSuffix(k, gitHubActionConfigurationOAuthToken) { + hash := hashGitHubToken(new) + return old == hash } - regions := make([]string, 0, len(configs)) - pipelineArtifactStores := make(map[string]*codepipeline.ArtifactStore) - for _, config := range configs { - region, store := expandArtifactStoreData(config.(map[string]interface{})) - regions = append(regions, region) - pipelineArtifactStores[region] = store + return false +} + +func hashGitHubToken(token string) string { + const gitHubTokenHashPrefix = "hash-" + + // Without this check, the value was getting encoded twice + if strings.HasPrefix(token, gitHubTokenHashPrefix) { + return token } - if len(regions) == 1 { - if regions[0] != "" { - return nil, errors.New("region cannot be set for a single-region CodePipeline") - } - } else { - for _, v := range regions { - if v == "" { - return nil, errors.New("region must be set for a cross-region CodePipeline") + sum := sha256.Sum256([]byte(token)) + return gitHubTokenHashPrefix + hex.EncodeToString(sum[:]) +} + +func expandPipelineDeclaration(d *schema.ResourceData) (*codepipeline.PipelineDeclaration, error) { + apiObject := &codepipeline.PipelineDeclaration{} + + if v, ok := d.GetOk("artifact_store"); ok && v.(*schema.Set).Len() > 0 { + artifactStores := expandArtifactStores(v.(*schema.Set).List()) + + switch n := len(artifactStores); n { + case 1: + for region, v := range artifactStores { + if region != "" { + return nil, errors.New("region cannot be set for a single-region CodePipeline") + } + apiObject.ArtifactStore = v } + + default: + for region := range artifactStores { + if region == "" { + return nil, errors.New("region must be set for a cross-region CodePipeline") + } + } + if n != v.(*schema.Set).Len() { + return nil, errors.New("only one Artifact Store can be defined per region for a cross-region CodePipeline") + } + apiObject.ArtifactStores = artifactStores } - if len(configs) != len(pipelineArtifactStores) { - return nil, errors.New("only one Artifact Store can be defined per region for a cross-region CodePipeline") - } } - return pipelineArtifactStores, nil + if v, ok := d.GetOk("name"); ok { + apiObject.Name = aws.String(v.(string)) + } + + if v, ok := d.GetOk("role_arn"); ok { + apiObject.RoleArn = aws.String(v.(string)) + } + + if v, ok := d.GetOk("stage"); ok && len(v.([]interface{})) > 0 { + apiObject.Stages = expandStageDeclarations(v.([]interface{})) + } + + return apiObject, nil } -func expandArtifactStoreData(data map[string]interface{}) (string, *codepipeline.ArtifactStore) { - pipelineArtifactStore := codepipeline.ArtifactStore{ - Location: aws.String(data["location"].(string)), - Type: aws.String(data["type"].(string)), - } - tek := data["encryption_key"].([]interface{}) - if len(tek) > 0 { - vk := tek[0].(map[string]interface{}) - ek := codepipeline.EncryptionKey{ - Type: aws.String(vk["type"].(string)), - Id: aws.String(vk["id"].(string)), - } - pipelineArtifactStore.EncryptionKey = &ek +func expandArtifactStore(tfMap map[string]interface{}) *codepipeline.ArtifactStore { + if tfMap == nil { + return nil + } + + apiObject := &codepipeline.ArtifactStore{} + + if v, ok := tfMap["encryption_key"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + apiObject.EncryptionKey = expandEncryptionKey(v[0].(map[string]interface{})) } - return data["region"].(string), &pipelineArtifactStore + if v, ok := tfMap["location"].(string); ok && v != "" { + apiObject.Location = aws.String(v) + } + + if v, ok := tfMap["type"].(string); ok && v != "" { + apiObject.Type = aws.String(v) + } + + return apiObject } -func flattenArtifactStore(artifactStore *codepipeline.ArtifactStore) []interface{} { - if artifactStore == nil { - return []interface{}{} +func expandArtifactStores(tfList []interface{}) map[string]*codepipeline.ArtifactStore { + if len(tfList) == 0 { + return nil } - values := map[string]interface{}{} - values["type"] = aws.StringValue(artifactStore.Type) - values["location"] = aws.StringValue(artifactStore.Location) - if artifactStore.EncryptionKey != nil { - as := map[string]interface{}{ - "id": aws.StringValue(artifactStore.EncryptionKey.Id), - "type": aws.StringValue(artifactStore.EncryptionKey.Type), + apiObjects := make(map[string]*codepipeline.ArtifactStore, 0) + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue } - values["encryption_key"] = []interface{}{as} + + apiObject := expandArtifactStore(tfMap) + + if apiObject == nil { + continue + } + + var region string + + if v, ok := tfMap["region"].(string); ok && v != "" { + region = v + } + + apiObjects[region] = apiObject } - return []interface{}{values} + + return apiObjects } -func flattenArtifactStores(artifactStores map[string]*codepipeline.ArtifactStore) []interface{} { - values := []interface{}{} - for region, artifactStore := range artifactStores { - store := flattenArtifactStore(artifactStore)[0].(map[string]interface{}) - store["region"] = region - values = append(values, store) +func expandEncryptionKey(tfMap map[string]interface{}) *codepipeline.EncryptionKey { + if tfMap == nil { + return nil } - return values -} -func expandStages(d *schema.ResourceData) []*codepipeline.StageDeclaration { - stages := d.Get("stage").([]interface{}) - pipelineStages := []*codepipeline.StageDeclaration{} - - for _, stage := range stages { - data := stage.(map[string]interface{}) - a := data["action"].([]interface{}) - actions := expandActions(a) - pipelineStages = append(pipelineStages, &codepipeline.StageDeclaration{ - Name: aws.String(data["name"].(string)), - Actions: actions, - }) + apiObject := &codepipeline.EncryptionKey{} + + if v, ok := tfMap["id"].(string); ok && v != "" { + apiObject.Id = aws.String(v) + } + + if v, ok := tfMap["type"].(string); ok && v != "" { + apiObject.Type = aws.String(v) } - return pipelineStages + + return apiObject } -func flattenStages(stages []*codepipeline.StageDeclaration, d *schema.ResourceData) []interface{} { - stagesList := []interface{}{} - for si, stage := range stages { - values := map[string]interface{}{} - values["name"] = aws.StringValue(stage.Name) - values["action"] = flattenStageActions(si, stage.Actions, d) - stagesList = append(stagesList, values) +func expandStageDeclaration(tfMap map[string]interface{}) *codepipeline.StageDeclaration { + if tfMap == nil { + return nil + } + + apiObject := &codepipeline.StageDeclaration{} + + if v, ok := tfMap["action"].([]interface{}); ok && len(v) > 0 { + apiObject.Actions = expandActionDeclarations(v) } - return stagesList + + if v, ok := tfMap["name"].(string); ok && v != "" { + apiObject.Name = aws.String(v) + } + + return apiObject } -func expandActions(a []interface{}) []*codepipeline.ActionDeclaration { - actions := []*codepipeline.ActionDeclaration{} - for _, config := range a { - data := config.(map[string]interface{}) +func expandStageDeclarations(tfList []interface{}) []*codepipeline.StageDeclaration { + if len(tfList) == 0 { + return nil + } - conf := flex.ExpandStringMap(data["configuration"].(map[string]interface{})) + var apiObjects []*codepipeline.StageDeclaration - action := codepipeline.ActionDeclaration{ - ActionTypeId: &codepipeline.ActionTypeId{ - Category: aws.String(data["category"].(string)), - Owner: aws.String(data["owner"].(string)), + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) - Provider: aws.String(data["provider"].(string)), - Version: aws.String(data["version"].(string)), - }, - Name: aws.String(data["name"].(string)), - Configuration: conf, + if !ok { + continue } - oa := data["output_artifacts"].([]interface{}) - if len(oa) > 0 { - outputArtifacts := expandActionsOutputArtifacts(oa) - action.OutputArtifacts = outputArtifacts + apiObject := expandStageDeclaration(tfMap) + if apiObject == nil { + continue } - ia := data["input_artifacts"].([]interface{}) - if len(ia) > 0 { - inputArtifacts := expandActionsInputArtifacts(ia) - action.InputArtifacts = inputArtifacts - } - ra := data["role_arn"].(string) - if ra != "" { - action.RoleArn = aws.String(ra) - } - ro := data["run_order"].(int) - if ro > 0 { - action.RunOrder = aws.Int64(int64(ro)) - } - r := data["region"].(string) - if r != "" { - action.Region = aws.String(r) - } - ns := data["namespace"].(string) - if len(ns) > 0 { - action.Namespace = aws.String(ns) - } - actions = append(actions, &action) + apiObjects = append(apiObjects, apiObject) } - return actions + + return apiObjects } -func flattenStageActions(si int, actions []*codepipeline.ActionDeclaration, d *schema.ResourceData) []interface{} { - actionsList := []interface{}{} - for ai, action := range actions { - values := map[string]interface{}{ - "category": aws.StringValue(action.ActionTypeId.Category), - "owner": aws.StringValue(action.ActionTypeId.Owner), - "provider": aws.StringValue(action.ActionTypeId.Provider), - "version": aws.StringValue(action.ActionTypeId.Version), - "name": aws.StringValue(action.Name), - } - if action.Configuration != nil { - config := aws.StringValueMap(action.Configuration) - - actionProvider := aws.StringValue(action.ActionTypeId.Provider) - if actionProvider == providerGitHub { - if _, ok := config[gitHubActionConfigurationOAuthToken]; ok { - // The AWS API returns "****" for the OAuthToken value. Pull the value from the configuration. - addr := fmt.Sprintf("stage.%d.action.%d.configuration.OAuthToken", si, ai) - config[gitHubActionConfigurationOAuthToken] = d.Get(addr).(string) - } - } +func expandActionDeclaration(tfMap map[string]interface{}) *codepipeline.ActionDeclaration { + if tfMap == nil { + return nil + } - values["configuration"] = config - } + apiObject := &codepipeline.ActionDeclaration{ + ActionTypeId: &codepipeline.ActionTypeId{}, + } - if len(action.OutputArtifacts) > 0 { - values["output_artifacts"] = flattenActionsOutputArtifacts(action.OutputArtifacts) - } + if v, ok := tfMap["category"].(string); ok && v != "" { + apiObject.ActionTypeId.Category = aws.String(v) + } - if len(action.InputArtifacts) > 0 { - values["input_artifacts"] = flattenActionsInputArtifacts(action.InputArtifacts) - } + if v, ok := tfMap["configuration"].(map[string]interface{}); ok && len(v) > 0 { + apiObject.Configuration = flex.ExpandStringMap(v) + } - if action.RoleArn != nil { - values["role_arn"] = aws.StringValue(action.RoleArn) - } + if v, ok := tfMap["input_artifacts"].([]interface{}); ok && len(v) > 0 { + apiObject.InputArtifacts = expandInputArtifacts(v) + } - if action.RunOrder != nil { - values["run_order"] = int(aws.Int64Value(action.RunOrder)) - } + if v, ok := tfMap["name"].(string); ok && v != "" { + apiObject.Name = aws.String(v) + } + + if v, ok := tfMap["namespace"].(string); ok && v != "" { + apiObject.Namespace = aws.String(v) + } + + if v, ok := tfMap["output_artifacts"].([]interface{}); ok && len(v) > 0 { + apiObject.OutputArtifacts = expandOutputArtifacts(v) + } + + if v, ok := tfMap["owner"].(string); ok && v != "" { + apiObject.ActionTypeId.Owner = aws.String(v) + } + + if v, ok := tfMap["provider"].(string); ok && v != "" { + apiObject.ActionTypeId.Provider = aws.String(v) + } + + if v, ok := tfMap["region"].(string); ok && v != "" { + apiObject.Region = aws.String(v) + } + + if v, ok := tfMap["role_arn"].(string); ok && v != "" { + apiObject.RoleArn = aws.String(v) + } + + if v, ok := tfMap["run_order"].(int); ok && v != 0 { + apiObject.RunOrder = aws.Int64(int64(v)) + } - if action.Region != nil { - values["region"] = aws.StringValue(action.Region) + if v, ok := tfMap["version"].(string); ok && v != "" { + apiObject.ActionTypeId.Version = aws.String(v) + } + + return apiObject +} + +func expandActionDeclarations(tfList []interface{}) []*codepipeline.ActionDeclaration { + if len(tfList) == 0 { + return nil + } + + var apiObjects []*codepipeline.ActionDeclaration + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue } - if action.Namespace != nil { - values["namespace"] = aws.StringValue(action.Namespace) + apiObject := expandActionDeclaration(tfMap) + + if apiObject == nil { + continue } - actionsList = append(actionsList, values) + apiObjects = append(apiObjects, apiObject) } - return actionsList + + return apiObjects } -func expandActionsOutputArtifacts(s []interface{}) []*codepipeline.OutputArtifact { - outputArtifacts := []*codepipeline.OutputArtifact{} - for _, artifact := range s { - if artifact == nil { +func expandInputArtifacts(tfList []interface{}) []*codepipeline.InputArtifact { + if len(tfList) == 0 { + return nil + } + + var apiObjects []*codepipeline.InputArtifact + + for _, v := range tfList { + v, ok := v.(string) + + if !ok { continue } - outputArtifacts = append(outputArtifacts, &codepipeline.OutputArtifact{ - Name: aws.String(artifact.(string)), - }) + + apiObject := &codepipeline.InputArtifact{ + Name: aws.String(v), + } + + apiObjects = append(apiObjects, apiObject) } - return outputArtifacts + + return apiObjects } -func flattenActionsOutputArtifacts(artifacts []*codepipeline.OutputArtifact) []string { - values := []string{} - for _, artifact := range artifacts { - values = append(values, aws.StringValue(artifact.Name)) +func expandOutputArtifacts(tfList []interface{}) []*codepipeline.OutputArtifact { + if len(tfList) == 0 { + return nil } - return values -} -func expandActionsInputArtifacts(s []interface{}) []*codepipeline.InputArtifact { - outputArtifacts := []*codepipeline.InputArtifact{} - for _, artifact := range s { - if artifact == nil { + var apiObjects []*codepipeline.OutputArtifact + + for _, v := range tfList { + v, ok := v.(string) + + if !ok { continue } - outputArtifacts = append(outputArtifacts, &codepipeline.InputArtifact{ - Name: aws.String(artifact.(string)), - }) + + apiObject := &codepipeline.OutputArtifact{ + Name: aws.String(v), + } + + apiObjects = append(apiObjects, apiObject) } - return outputArtifacts + + return apiObjects } -func flattenActionsInputArtifacts(artifacts []*codepipeline.InputArtifact) []string { - values := []string{} - for _, artifact := range artifacts { - values = append(values, aws.StringValue(artifact.Name)) +func flattenArtifactStore(apiObject *codepipeline.ArtifactStore) map[string]interface{} { + if apiObject == nil { + return nil } - return values -} -func resourceRead(d *schema.ResourceData, meta interface{}) error { - conn := meta.(*conns.AWSClient).CodePipelineConn - defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig - ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig + tfMap := map[string]interface{}{} - resp, err := conn.GetPipeline(&codepipeline.GetPipelineInput{ - Name: aws.String(d.Id()), - }) + if v := apiObject.EncryptionKey; v != nil { + tfMap["encryption_key"] = []interface{}{flattenEncryptionKey(v)} + } - if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, codepipeline.ErrCodePipelineNotFoundException) { - create.LogNotFoundRemoveState(names.CodePipeline, create.ErrActionReading, ResNamePipeline, d.Id()) - d.SetId("") - return nil + if v := apiObject.Location; v != nil { + tfMap["location"] = aws.StringValue(v) } - if err != nil { - return create.Error(names.CodePipeline, create.ErrActionReading, ResNamePipeline, d.Id(), err) + if v := apiObject.Type; v != nil { + tfMap["type"] = aws.StringValue(v) } - metadata := resp.Metadata - pipeline := resp.Pipeline + return tfMap +} - if pipeline.ArtifactStore != nil { - if err := d.Set("artifact_store", flattenArtifactStore(pipeline.ArtifactStore)); err != nil { - return err - } - } else if pipeline.ArtifactStores != nil { - if err := d.Set("artifact_store", flattenArtifactStores(pipeline.ArtifactStores)); err != nil { - return err +func flattenArtifactStores(apiObjects map[string]*codepipeline.ArtifactStore) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for region, apiObject := range apiObjects { + if apiObject == nil { + continue } + + tfMap := flattenArtifactStore(apiObject) + tfMap["region"] = region + + tfList = append(tfList, tfMap) } - if err := d.Set("stage", flattenStages(pipeline.Stages, d)); err != nil { - return err + return tfList +} + +func flattenEncryptionKey(apiObject *codepipeline.EncryptionKey) map[string]interface{} { + if apiObject == nil { + return nil } - arn := aws.StringValue(metadata.PipelineArn) - d.Set("arn", arn) - d.Set("name", pipeline.Name) - d.Set("role_arn", pipeline.RoleArn) + tfMap := map[string]interface{}{} - tags, err := ListTags(conn, arn) + if v := apiObject.Id; v != nil { + tfMap["id"] = aws.StringValue(v) + } - if err != nil { - return fmt.Errorf("error listing tags for CodePipeline (%s): %w", arn, err) + if v := apiObject.Type; v != nil { + tfMap["type"] = aws.StringValue(v) } - tags = tags.IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + return tfMap +} - //lintignore:AWSR002 - if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %w", err) +func flattenStageDeclaration(d *schema.ResourceData, i int, apiObject *codepipeline.StageDeclaration) map[string]interface{} { + if apiObject == nil { + return nil } - if err := d.Set("tags_all", tags.Map()); err != nil { - return fmt.Errorf("error setting tags_all: %w", err) + tfMap := map[string]interface{}{} + + if v := apiObject.Actions; v != nil { + tfMap["action"] = flattenActionDeclarations(d, i, v) } - return nil + if v := apiObject.Name; v != nil { + tfMap["name"] = aws.StringValue(v) + } + + return tfMap } -func resourceUpdate(d *schema.ResourceData, meta interface{}) error { - conn := meta.(*conns.AWSClient).CodePipelineConn +func flattenStageDeclarations(d *schema.ResourceData, apiObjects []*codepipeline.StageDeclaration) []interface{} { + if len(apiObjects) == 0 { + return nil + } - if d.HasChangesExcept("tags", "tags_all") { - pipeline, err := expand(d) - if err != nil { - return err + var tfList []interface{} + + for i, apiObject := range apiObjects { + if apiObject == nil { + continue } - params := &codepipeline.UpdatePipelineInput{ - Pipeline: pipeline, + + tfList = append(tfList, flattenStageDeclaration(d, i, apiObject)) + } + + return tfList +} + +func flattenActionDeclaration(d *schema.ResourceData, i, j int, apiObject *codepipeline.ActionDeclaration) map[string]interface{} { + if apiObject == nil { + return nil + } + + var actionProvider string + tfMap := map[string]interface{}{} + + if apiObject := apiObject.ActionTypeId; apiObject != nil { + if v := apiObject.Category; v != nil { + tfMap["category"] = aws.StringValue(v) } - _, err = conn.UpdatePipeline(params) - if err != nil { - return fmt.Errorf("[ERROR] Error updating CodePipeline (%s): %w", d.Id(), err) + if v := apiObject.Owner; v != nil { + tfMap["owner"] = aws.StringValue(v) + } + + if v := apiObject.Provider; v != nil { + actionProvider = aws.StringValue(v) + tfMap["provider"] = actionProvider + } + + if v := apiObject.Version; v != nil { + tfMap["version"] = aws.StringValue(v) } } - arn := d.Get("arn").(string) - if d.HasChange("tags_all") { - o, n := d.GetChange("tags_all") + if v := apiObject.Configuration; v != nil { + v := aws.StringValueMap(v) - if err := UpdateTags(conn, arn, o, n); err != nil { - return fmt.Errorf("error updating CodePipeline (%s) tags: %w", arn, err) + // The AWS API returns "****" for the OAuthToken value. Copy the value from the configuration. + if actionProvider == providerGitHub { + if _, ok := v[gitHubActionConfigurationOAuthToken]; ok { + key := fmt.Sprintf("stage.%d.action.%d.configuration.OAuthToken", i, j) + v[gitHubActionConfigurationOAuthToken] = d.Get(key).(string) + } } + + tfMap["configuration"] = v } - return resourceRead(d, meta) -} + if v := apiObject.InputArtifacts; len(v) > 0 { + tfMap["input_artifacts"] = flattenInputArtifacts(v) + } -func resourceDelete(d *schema.ResourceData, meta interface{}) error { - conn := meta.(*conns.AWSClient).CodePipelineConn + if v := apiObject.Name; v != nil { + tfMap["name"] = aws.StringValue(v) + } - _, err := conn.DeletePipeline(&codepipeline.DeletePipelineInput{ - Name: aws.String(d.Id()), - }) + if v := apiObject.Namespace; v != nil { + tfMap["namespace"] = aws.StringValue(v) + } - if tfawserr.ErrCodeEquals(err, codepipeline.ErrCodePipelineNotFoundException) { - return nil + if v := apiObject.OutputArtifacts; len(v) > 0 { + tfMap["output_artifacts"] = flattenOutputArtifacts(v) } - if err != nil { - return fmt.Errorf("error deleting CodePipeline (%s): %w", d.Id(), err) + if v := apiObject.Region; v != nil { + tfMap["region"] = aws.StringValue(v) } - return err + if v := apiObject.RoleArn; v != nil { + tfMap["role_arn"] = aws.StringValue(v) + } + + if v := apiObject.RunOrder; v != nil { + tfMap["run_order"] = aws.Int64Value(v) + } + + return tfMap } -func resourceValidateActionProvider(i interface{}, path cty.Path) diag.Diagnostics { - v, ok := i.(string) - if !ok { - return diag.Errorf("expected type to be string") +func flattenActionDeclarations(d *schema.ResourceData, i int, apiObjects []*codepipeline.ActionDeclaration) []interface{} { + if len(apiObjects) == 0 { + return nil } - if v == providerGitHub { - return diag.Diagnostics{ - diag.Diagnostic{ - Severity: diag.Warning, - Summary: "The CodePipeline GitHub version 1 action provider is deprecated.", - Detail: "Use a GitHub version 2 action (with a CodeStar Connection `aws_codestarconnections_connection`) instead. See https://docs.aws.amazon.com/codepipeline/latest/userguide/update-github-action-connections.html", - }, + var tfList []interface{} + + for j, apiObject := range apiObjects { + if apiObject == nil { + continue } + + tfList = append(tfList, flattenActionDeclaration(d, i, j, apiObject)) } - return nil + return tfList } -func suppressStageActionConfiguration(k, old, new string, d *schema.ResourceData) bool { - parts := strings.Split(k, ".") - parts = parts[:len(parts)-2] - providerAddr := strings.Join(append(parts, "provider"), ".") - provider := d.Get(providerAddr).(string) +func flattenInputArtifacts(apiObjects []*codepipeline.InputArtifact) []string { + if len(apiObjects) == 0 { + return nil + } - if provider == providerGitHub && strings.HasSuffix(k, gitHubActionConfigurationOAuthToken) { - hash := hashGitHubToken(new) - return old == hash + var tfList []*string + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, apiObject.Name) } - return false + return aws.StringValueSlice(tfList) } -const gitHubTokenHashPrefix = "hash-" +func flattenOutputArtifacts(apiObjects []*codepipeline.OutputArtifact) []string { + if len(apiObjects) == 0 { + return nil + } -func hashGitHubToken(token string) string { - // Without this check, the value was getting encoded twice - if strings.HasPrefix(token, gitHubTokenHashPrefix) { - return token + var tfList []*string + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, apiObject.Name) } - sum := sha256.Sum256([]byte(token)) - return gitHubTokenHashPrefix + hex.EncodeToString(sum[:]) + + return aws.StringValueSlice(tfList) } diff --git a/internal/service/codepipeline/codepipeline_test.go b/internal/service/codepipeline/codepipeline_test.go index f9c83ddda65..70979d6f255 100644 --- a/internal/service/codepipeline/codepipeline_test.go +++ b/internal/service/codepipeline/codepipeline_test.go @@ -6,16 +6,15 @@ import ( "regexp" "testing" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/codepipeline" "github.com/aws/aws-sdk-go/service/codestarconnections" - "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/hashicorp/terraform-provider-aws/internal/acctest" "github.com/hashicorp/terraform-provider-aws/internal/conns" tfcodepipeline "github.com/hashicorp/terraform-provider-aws/internal/service/codepipeline" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) func TestAccCodePipeline_basic(t *testing.T) { @@ -32,12 +31,12 @@ func TestAccCodePipeline_basic(t *testing.T) { }, ErrorCheck: acctest.ErrorCheck(t, codepipeline.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: testAccCheckDestroy, + CheckDestroy: testAccCheckPipelineDestroy, Steps: []resource.TestStep{ { Config: testAccCodePipelineConfig_basic(name), Check: resource.ComposeTestCheckFunc( - testAccCheckExists(resourceName, &p1), + testAccCheckPipelineExists(resourceName, &p1), resource.TestCheckResourceAttrPair(resourceName, "role_arn", "aws_iam_role.codepipeline_role", "arn"), acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "codepipeline", regexp.MustCompile(fmt.Sprintf("test-pipeline-%s", name))), resource.TestCheckResourceAttr(resourceName, "artifact_store.#", "1"), @@ -87,7 +86,7 @@ func TestAccCodePipeline_basic(t *testing.T) { { Config: testAccCodePipelineConfig_basicUpdated(name), Check: resource.ComposeTestCheckFunc( - testAccCheckExists(resourceName, &p2), + testAccCheckPipelineExists(resourceName, &p2), resource.TestCheckResourceAttr(resourceName, "stage.#", "2"), @@ -137,13 +136,13 @@ func TestAccCodePipeline_disappears(t *testing.T) { }, ErrorCheck: acctest.ErrorCheck(t, codepipeline.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: testAccCheckDestroy, + CheckDestroy: testAccCheckPipelineDestroy, Steps: []resource.TestStep{ { Config: testAccCodePipelineConfig_basic(name), Check: resource.ComposeTestCheckFunc( - testAccCheckExists(resourceName, &p), - acctest.CheckResourceDisappears(acctest.Provider, tfcodepipeline.ResourceCodePipeline(), resourceName), + testAccCheckPipelineExists(resourceName, &p), + acctest.CheckResourceDisappears(acctest.Provider, tfcodepipeline.ResourcePipeline(), resourceName), ), ExpectNonEmptyPlan: true, }, @@ -164,12 +163,12 @@ func TestAccCodePipeline_emptyStageArtifacts(t *testing.T) { }, ErrorCheck: acctest.ErrorCheck(t, codepipeline.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: testAccCheckDestroy, + CheckDestroy: testAccCheckPipelineDestroy, Steps: []resource.TestStep{ { Config: testAccCodePipelineConfig_emptyStageArtifacts(name), Check: resource.ComposeTestCheckFunc( - testAccCheckExists(resourceName, &p), + testAccCheckPipelineExists(resourceName, &p), acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "codepipeline", regexp.MustCompile(fmt.Sprintf("test-pipeline-%s$", name))), resource.TestCheckResourceAttr(resourceName, "artifact_store.#", "1"), resource.TestCheckResourceAttr(resourceName, "stage.#", "2"), @@ -206,12 +205,12 @@ func TestAccCodePipeline_deployWithServiceRole(t *testing.T) { }, ErrorCheck: acctest.ErrorCheck(t, codepipeline.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: testAccCheckDestroy, + CheckDestroy: testAccCheckPipelineDestroy, Steps: []resource.TestStep{ { Config: testAccCodePipelineConfig_deployServiceRole(name), Check: resource.ComposeTestCheckFunc( - testAccCheckExists(resourceName, &p), + testAccCheckPipelineExists(resourceName, &p), resource.TestCheckResourceAttr(resourceName, "stage.2.name", "Deploy"), resource.TestCheckResourceAttr(resourceName, "stage.2.action.0.category", "Deploy"), resource.TestCheckResourceAttrPair(resourceName, "stage.2.action.0.role_arn", "aws_iam_role.codepipeline_action_role", "arn"), @@ -239,12 +238,12 @@ func TestAccCodePipeline_tags(t *testing.T) { }, ErrorCheck: acctest.ErrorCheck(t, codepipeline.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: testAccCheckDestroy, + CheckDestroy: testAccCheckPipelineDestroy, Steps: []resource.TestStep{ { Config: testAccCodePipelineConfig_tags(name, "tag1value", "tag2value"), Check: resource.ComposeTestCheckFunc( - testAccCheckExists(resourceName, &p1), + testAccCheckPipelineExists(resourceName, &p1), resource.TestCheckResourceAttr(resourceName, "tags.%", "3"), resource.TestCheckResourceAttr(resourceName, "tags.Name", fmt.Sprintf("test-pipeline-%s", name)), resource.TestCheckResourceAttr(resourceName, "tags.tag1", "tag1value"), @@ -259,7 +258,7 @@ func TestAccCodePipeline_tags(t *testing.T) { { Config: testAccCodePipelineConfig_tags(name, "tag1valueUpdate", "tag2valueUpdate"), Check: resource.ComposeTestCheckFunc( - testAccCheckExists(resourceName, &p2), + testAccCheckPipelineExists(resourceName, &p2), resource.TestCheckResourceAttr(resourceName, "tags.%", "3"), resource.TestCheckResourceAttr(resourceName, "tags.Name", fmt.Sprintf("test-pipeline-%s", name)), resource.TestCheckResourceAttr(resourceName, "tags.tag1", "tag1valueUpdate"), @@ -274,7 +273,7 @@ func TestAccCodePipeline_tags(t *testing.T) { { Config: testAccCodePipelineConfig_basic(name), Check: resource.ComposeTestCheckFunc( - testAccCheckExists(resourceName, &p3), + testAccCheckPipelineExists(resourceName, &p3), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), ), }, @@ -297,12 +296,12 @@ func TestAccCodePipeline_MultiRegion_basic(t *testing.T) { }, ErrorCheck: acctest.ErrorCheck(t, codepipeline.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5FactoriesAlternate(t), - CheckDestroy: testAccCheckDestroy, + CheckDestroy: testAccCheckPipelineDestroy, Steps: []resource.TestStep{ { Config: testAccCodePipelineConfig_multiregion(name), Check: resource.ComposeTestCheckFunc( - testAccCheckExists(resourceName, &p), + testAccCheckPipelineExists(resourceName, &p), resource.TestCheckResourceAttr(resourceName, "artifact_store.#", "2"), resource.TestCheckResourceAttr(resourceName, "stage.1.name", "Build"), @@ -338,12 +337,12 @@ func TestAccCodePipeline_MultiRegion_update(t *testing.T) { }, ErrorCheck: acctest.ErrorCheck(t, codepipeline.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5FactoriesAlternate(t), - CheckDestroy: testAccCheckDestroy, + CheckDestroy: testAccCheckPipelineDestroy, Steps: []resource.TestStep{ { Config: testAccCodePipelineConfig_multiregion(name), Check: resource.ComposeTestCheckFunc( - testAccCheckExists(resourceName, &p1), + testAccCheckPipelineExists(resourceName, &p1), resource.TestCheckResourceAttr(resourceName, "artifact_store.#", "2"), resource.TestCheckResourceAttr(resourceName, "stage.1.name", "Build"), @@ -357,7 +356,7 @@ func TestAccCodePipeline_MultiRegion_update(t *testing.T) { { Config: testAccCodePipelineConfig_multiregionUpdated(name), Check: resource.ComposeTestCheckFunc( - testAccCheckExists(resourceName, &p2), + testAccCheckPipelineExists(resourceName, &p2), resource.TestCheckResourceAttr(resourceName, "artifact_store.#", "2"), resource.TestCheckResourceAttr(resourceName, "stage.1.name", "Build"), @@ -393,12 +392,12 @@ func TestAccCodePipeline_MultiRegion_convertSingleRegion(t *testing.T) { }, ErrorCheck: acctest.ErrorCheck(t, codepipeline.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5FactoriesAlternate(t), - CheckDestroy: testAccCheckDestroy, + CheckDestroy: testAccCheckPipelineDestroy, Steps: []resource.TestStep{ { Config: testAccCodePipelineConfig_basic(name), Check: resource.ComposeTestCheckFunc( - testAccCheckExists(resourceName, &p1), + testAccCheckPipelineExists(resourceName, &p1), resource.TestCheckResourceAttr(resourceName, "artifact_store.#", "1"), resource.TestCheckResourceAttr(resourceName, "stage.1.name", "Build"), @@ -410,7 +409,7 @@ func TestAccCodePipeline_MultiRegion_convertSingleRegion(t *testing.T) { { Config: testAccCodePipelineConfig_multiregion(name), Check: resource.ComposeTestCheckFunc( - testAccCheckExists(resourceName, &p2), + testAccCheckPipelineExists(resourceName, &p2), resource.TestCheckResourceAttr(resourceName, "artifact_store.#", "2"), resource.TestCheckResourceAttr(resourceName, "stage.1.name", "Build"), @@ -424,7 +423,7 @@ func TestAccCodePipeline_MultiRegion_convertSingleRegion(t *testing.T) { { Config: testAccCodePipelineConfig_backToBasic(name), Check: resource.ComposeTestCheckFunc( - testAccCheckExists(resourceName, &p1), + testAccCheckPipelineExists(resourceName, &p1), resource.TestCheckResourceAttr(resourceName, "artifact_store.#", "1"), resource.TestCheckResourceAttr(resourceName, "stage.1.name", "Build"), @@ -456,12 +455,12 @@ func TestAccCodePipeline_withNamespace(t *testing.T) { }, ErrorCheck: acctest.ErrorCheck(t, codepipeline.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: testAccCheckDestroy, + CheckDestroy: testAccCheckPipelineDestroy, Steps: []resource.TestStep{ { Config: testAccCodePipelineConfig_namespace(name), Check: resource.ComposeTestCheckFunc( - testAccCheckExists(resourceName, &p1), + testAccCheckPipelineExists(resourceName, &p1), acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "codepipeline", regexp.MustCompile(fmt.Sprintf("test-pipeline-%s", name))), resource.TestCheckResourceAttr(resourceName, "stage.0.action.0.namespace", "SourceVariables"), ), @@ -489,12 +488,12 @@ func TestAccCodePipeline_withGitHubV1SourceAction(t *testing.T) { }, ErrorCheck: acctest.ErrorCheck(t, codepipeline.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: testAccCheckDestroy, + CheckDestroy: testAccCheckPipelineDestroy, Steps: []resource.TestStep{ { Config: testAccCodePipelineConfig_gitHubv1SourceAction(name, githubToken), Check: resource.ComposeTestCheckFunc( - testAccCheckExists(resourceName, &v), + testAccCheckPipelineExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "stage.#", "2"), @@ -522,7 +521,7 @@ func TestAccCodePipeline_withGitHubV1SourceAction(t *testing.T) { { Config: testAccCodePipelineConfig_gitHubv1SourceActionUpdated(name, githubToken), Check: resource.ComposeTestCheckFunc( - testAccCheckExists(resourceName, &v), + testAccCheckPipelineExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "stage.#", "2"), @@ -551,7 +550,55 @@ func TestAccCodePipeline_withGitHubV1SourceAction(t *testing.T) { }) } -func testAccCheckExists(n string, pipeline *codepipeline.PipelineDeclaration) resource.TestCheckFunc { +func TestAccCodePipeline_ecr(t *testing.T) { + var p codepipeline.PipelineDeclaration + name := sdkacctest.RandString(10) + resourceName := "aws_codepipeline.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + testAccPreCheckSupported(t) + acctest.PreCheckPartitionHasService(codestarconnections.EndpointsID, t) + }, + ErrorCheck: acctest.ErrorCheck(t, codepipeline.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckPipelineDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCodePipelineConfig_ecr(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckPipelineExists(resourceName, &p), + resource.TestCheckResourceAttr(resourceName, "stage.#", "2"), + resource.TestCheckResourceAttr(resourceName, "stage.0.name", "Source"), + resource.TestCheckResourceAttr(resourceName, "stage.0.action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "stage.0.action.0.name", "Source"), + resource.TestCheckResourceAttr(resourceName, "stage.0.action.0.category", "Source"), + resource.TestCheckResourceAttr(resourceName, "stage.0.action.0.owner", "AWS"), + resource.TestCheckResourceAttr(resourceName, "stage.0.action.0.provider", "ECR"), + resource.TestCheckResourceAttr(resourceName, "stage.0.action.0.version", "1"), + resource.TestCheckResourceAttr(resourceName, "stage.0.action.0.input_artifacts.#", "0"), + resource.TestCheckResourceAttr(resourceName, "stage.0.action.0.output_artifacts.#", "1"), + resource.TestCheckResourceAttr(resourceName, "stage.0.action.0.output_artifacts.0", "test"), + resource.TestCheckResourceAttr(resourceName, "stage.0.action.0.configuration.%", "2"), + resource.TestCheckResourceAttr(resourceName, "stage.0.action.0.configuration.RepositoryName", "my-image-repo"), + resource.TestCheckResourceAttr(resourceName, "stage.0.action.0.configuration.ImageTag", "latest"), + resource.TestCheckResourceAttr(resourceName, "stage.0.action.0.role_arn", ""), + resource.TestCheckResourceAttr(resourceName, "stage.0.action.0.run_order", "1"), + resource.TestCheckResourceAttr(resourceName, "stage.0.action.0.region", ""), + resource.TestCheckResourceAttr(resourceName, "stage.1.name", "Build"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckPipelineExists(n string, v *codepipeline.PipelineDeclaration) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -564,20 +611,19 @@ func testAccCheckExists(n string, pipeline *codepipeline.PipelineDeclaration) re conn := acctest.Provider.Meta().(*conns.AWSClient).CodePipelineConn - out, err := conn.GetPipeline(&codepipeline.GetPipelineInput{ - Name: aws.String(rs.Primary.ID), - }) + output, err := tfcodepipeline.FindPipelineByName(context.Background(), conn, rs.Primary.ID) + if err != nil { return err } - *pipeline = *out.Pipeline + *v = *output.Pipeline return nil } } -func testAccCheckDestroy(s *terraform.State) error { +func testAccCheckPipelineDestroy(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).CodePipelineConn for _, rs := range s.RootModule().Resources { @@ -585,17 +631,17 @@ func testAccCheckDestroy(s *terraform.State) error { continue } - _, err := conn.GetPipeline(&codepipeline.GetPipelineInput{ - Name: aws.String(rs.Primary.ID), - }) + _, err := tfcodepipeline.FindPipelineByName(context.Background(), conn, rs.Primary.ID) - if err == nil { - return fmt.Errorf("Expected AWS CodePipeline to be gone, but was still found") - } - if tfawserr.ErrCodeEquals(err, "PipelineNotFoundException") { + if tfresource.NotFound(err) { continue } - return err + + if err != nil { + return err + } + + return fmt.Errorf("CodePipeline %s still exists", rs.Primary.ID) } return nil @@ -1556,123 +1602,59 @@ resource "aws_codepipeline" "test" { `, rName, githubToken)) } -func TestExpandArtifactStoresValidation(t *testing.T) { - cases := []struct { - Name string - Input []interface{} - ExpectedError string - }{ - { - Name: "Single-region", - Input: []interface{}{ - map[string]interface{}{ - "location": "", - "type": "", - "encryption_key": []interface{}{}, - "region": "", - }, - }, - }, - { - Name: "Single-region, names region", - Input: []interface{}{ - map[string]interface{}{ - "location": "", - "type": "", - "encryption_key": []interface{}{}, - "region": "us-west-2", //lintignore:AWSAT003 - }, - }, - ExpectedError: "region cannot be set for a single-region CodePipeline", - }, - { - Name: "Cross-region", - Input: []interface{}{ - map[string]interface{}{ - "location": "", - "type": "", - "encryption_key": []interface{}{}, - "region": "us-west-2", //lintignore:AWSAT003 - }, - map[string]interface{}{ - "location": "", - "type": "", - "encryption_key": []interface{}{}, - "region": "us-east-1", //lintignore:AWSAT003 - }, - }, - }, - { - Name: "Cross-region, no regions", - Input: []interface{}{ - map[string]interface{}{ - "location": "", - "type": "", - "encryption_key": []interface{}{}, - "region": "", - }, - map[string]interface{}{ - "location": "", - "type": "", - "encryption_key": []interface{}{}, - "region": "", - }, - }, - ExpectedError: "region must be set for a cross-region CodePipeline", - }, - { - Name: "Cross-region, not all regions", - Input: []interface{}{ - map[string]interface{}{ - "location": "", - "type": "", - "encryption_key": []interface{}{}, - "region": "us-west-2", //lintignore:AWSAT003 - }, - map[string]interface{}{ - "location": "", - "type": "", - "encryption_key": []interface{}{}, - "region": "", - }, - }, - ExpectedError: "region must be set for a cross-region CodePipeline", - }, - { - Name: "Duplicate regions", - Input: []interface{}{ - map[string]interface{}{ - "location": "", - "type": "", - "encryption_key": []interface{}{}, - "region": "us-west-2", //lintignore:AWSAT003 - }, - map[string]interface{}{ - "location": "", - "type": "", - "encryption_key": []interface{}{}, - "region": "us-west-2", //lintignore:AWSAT003 - }, - }, - ExpectedError: "only one Artifact Store can be defined per region for a cross-region CodePipeline", - }, - } +func testAccCodePipelineConfig_ecr(rName string) string { // nosemgrep:ci.codepipeline-in-func-name + return acctest.ConfigCompose( + testAccS3DefaultBucket(rName), + testAccServiceIAMRole(rName), + fmt.Sprintf(` +resource "aws_codepipeline" "test" { + name = "test-pipeline-%[1]s" + role_arn = aws_iam_role.codepipeline_role.arn - for _, tc := range cases { - tc := tc - _, err := tfcodepipeline.ExpandArtifactStores(tc.Input) - if tc.ExpectedError == "" { - if err != nil { - t.Errorf("%s: Did not expect an error, but got: %s", tc.Name, err) - } - } else { - if err == nil { - t.Errorf("%s: Expected an error, but did not get one", tc.Name) - } else { - if err.Error() != tc.ExpectedError { - t.Errorf("%s: Expected error %q, got %s", tc.Name, tc.ExpectedError, err) - } - } - } - } + artifact_store { + location = aws_s3_bucket.test.bucket + type = "S3" + + encryption_key { + id = "1234" + type = "KMS" + } + } + + stage { + name = "Source" + + action { + name = "Source" + category = "Source" + owner = "AWS" + provider = "ECR" + version = "1" + output_artifacts = ["test"] + + configuration = { + RepositoryName = "my-image-repo" + ImageTag = "latest" + } + } + } + + stage { + name = "Build" + + action { + name = "Build" + category = "Build" + owner = "AWS" + provider = "CodeBuild" + input_artifacts = ["test"] + version = "1" + + configuration = { + ProjectName = "test" + } + } + } +} +`, rName)) } diff --git a/internal/service/codepipeline/custom_action_type.go b/internal/service/codepipeline/custom_action_type.go new file mode 100644 index 00000000000..33ab9ce525d --- /dev/null +++ b/internal/service/codepipeline/custom_action_type.go @@ -0,0 +1,615 @@ +package codepipeline + +import ( + "context" + "fmt" + "log" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/service/codepipeline" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/internal/verify" +) + +func ResourceCustomActionType() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceCustomActionTypeCreate, + ReadWithoutTimeout: resourceCustomActionTypeRead, + UpdateWithoutTimeout: resourceCustomActionTypeUpdate, + DeleteWithoutTimeout: resourceCustomActionTypeDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "category": { + Type: schema.TypeString, + ForceNew: true, + Required: true, + ValidateFunc: validation.StringInSlice(codepipeline.ActionCategory_Values(), false), + }, + "configuration_property": { + Type: schema.TypeList, + MaxItems: 10, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "description": { + Type: schema.TypeString, + Optional: true, + }, + "key": { + Type: schema.TypeBool, + Required: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "queryable": { + Type: schema.TypeBool, + Optional: true, + }, + "required": { + Type: schema.TypeBool, + Required: true, + }, + "secret": { + Type: schema.TypeBool, + Required: true, + }, + "type": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(codepipeline.ActionConfigurationPropertyType_Values(), false), + }, + }, + }, + }, + "input_artifact_details": { + Type: schema.TypeList, + ForceNew: true, + Required: true, + MinItems: 1, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "maximum_count": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(0, 5), + }, + "minimum_count": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(0, 5), + }, + }, + }, + }, + "output_artifact_details": { + Type: schema.TypeList, + ForceNew: true, + Required: true, + MinItems: 1, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "maximum_count": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(0, 5), + }, + "minimum_count": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(0, 5), + }, + }, + }, + }, + "owner": { + Type: schema.TypeString, + Computed: true, + }, + "provider_name": { + Type: schema.TypeString, + ForceNew: true, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 35), + }, + "settings": { + Type: schema.TypeList, + ForceNew: true, + Optional: true, + MinItems: 1, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "entity_url_template": { + Type: schema.TypeString, + Optional: true, + }, + "execution_url_template": { + Type: schema.TypeString, + Optional: true, + }, + "revision_url_template": { + Type: schema.TypeString, + Optional: true, + }, + "third_party_configuration_url": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "tags": tftags.TagsSchema(), + "tags_all": tftags.TagsSchemaComputed(), + "version": { + Type: schema.TypeString, + ForceNew: true, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 9), + }, + }, + + CustomizeDiff: verify.SetTagsDiff, + } +} + +func resourceCustomActionTypeCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).CodePipelineConn + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) + + category := d.Get("category").(string) + provider := d.Get("provider_name").(string) + version := d.Get("version").(string) + id := CustomActionTypeCreateResourceID(category, provider, version) + input := &codepipeline.CreateCustomActionTypeInput{ + Category: aws.String(category), + Provider: aws.String(provider), + Version: aws.String(version), + } + + if v, ok := d.GetOk("configuration_property"); ok && len(v.([]interface{})) > 0 { + input.ConfigurationProperties = expandActionConfigurationProperties(v.([]interface{})) + } + + if v, ok := d.GetOk("input_artifact_details"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.InputArtifactDetails = expandArtifactDetails(v.([]interface{})[0].(map[string]interface{})) + } + + if v, ok := d.GetOk("output_artifact_details"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.OutputArtifactDetails = expandArtifactDetails(v.([]interface{})[0].(map[string]interface{})) + } + + if v, ok := d.GetOk("settings"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.Settings = expandActionTypeSettings(v.([]interface{})[0].(map[string]interface{})) + } + + if len(tags) > 0 { + input.Tags = Tags(tags.IgnoreAWS()) + } + + _, err := conn.CreateCustomActionTypeWithContext(ctx, input) + + if err != nil { + return diag.Errorf("creating CodePipeline Custom Action Type (%s): %s", id, err) + } + + d.SetId(id) + + return resourceCustomActionTypeRead(ctx, d, meta) +} + +func resourceCustomActionTypeRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).CodePipelineConn + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig + + category, provider, version, err := CustomActionTypeParseResourceID(d.Id()) + + if err != nil { + return diag.FromErr(err) + } + + actionType, err := FindCustomActionTypeByThreePartKey(ctx, conn, category, provider, version) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] CodePipeline Custom Action Type %s not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.Errorf("reading CodePipeline Custom Action Type (%s): %s", d.Id(), err) + } + + arn := arn.ARN{ + Partition: meta.(*conns.AWSClient).Partition, + Service: codepipeline.ServiceName, + Region: meta.(*conns.AWSClient).Region, + AccountID: meta.(*conns.AWSClient).AccountID, + Resource: fmt.Sprintf("actiontype:%s/%s/%s/%s", codepipeline.ActionOwnerCustom, category, provider, version), + }.String() + d.Set("arn", arn) + d.Set("category", actionType.Id.Category) + if err := d.Set("configuration_property", flattenActionConfigurationProperties(d, actionType.ActionConfigurationProperties)); err != nil { + return diag.Errorf("setting configuration_property: %s", err) + } + if actionType.InputArtifactDetails != nil { + if err := d.Set("input_artifact_details", []interface{}{flattenArtifactDetails(actionType.InputArtifactDetails)}); err != nil { + return diag.Errorf("setting input_artifact_details: %s", err) + } + } else { + d.Set("input_artifact_details", nil) + } + if actionType.OutputArtifactDetails != nil { + if err := d.Set("output_artifact_details", []interface{}{flattenArtifactDetails(actionType.OutputArtifactDetails)}); err != nil { + return diag.Errorf("setting output_artifact_details: %s", err) + } + } else { + d.Set("output_artifact_details", nil) + } + d.Set("owner", actionType.Id.Owner) + d.Set("provider_name", actionType.Id.Provider) + if actionType.Settings != nil && + // Service can return empty ({}) Settings. + (actionType.Settings.EntityUrlTemplate != nil || actionType.Settings.ExecutionUrlTemplate != nil || actionType.Settings.RevisionUrlTemplate != nil || actionType.Settings.ThirdPartyConfigurationUrl != nil) { + if err := d.Set("settings", []interface{}{flattenActionTypeSettings(actionType.Settings)}); err != nil { + return diag.Errorf("setting settings: %s", err) + } + } else { + d.Set("settings", nil) + } + d.Set("version", actionType.Id.Version) + + tags, err := ListTagsWithContext(ctx, conn, arn) + + if err != nil { + return diag.Errorf("listing tags for CodePipeline Custom Action Type (%s): %s", arn, err) + } + + tags = tags.IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return diag.Errorf("setting tags: %s", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return diag.Errorf("setting tags_all: %s", err) + } + + return nil +} + +func resourceCustomActionTypeUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).CodePipelineConn + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + arn := d.Get("arn").(string) + + if err := UpdateTagsWithContext(ctx, conn, arn, o, n); err != nil { + return diag.Errorf("updating CodePipeline Custom Action Type (%s) tags: %s", arn, err) + } + } + + return resourceCustomActionTypeRead(ctx, d, meta) +} + +func resourceCustomActionTypeDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).CodePipelineConn + + category, provider, version, err := CustomActionTypeParseResourceID(d.Id()) + + if err != nil { + return diag.FromErr(err) + } + + log.Printf("[INFO] Deleting CodePipeline Custom Action Type: %s", d.Id()) + _, err = conn.DeleteCustomActionTypeWithContext(ctx, &codepipeline.DeleteCustomActionTypeInput{ + Category: aws.String(category), + Provider: aws.String(provider), + Version: aws.String(version), + }) + + if tfawserr.ErrCodeEquals(err, codepipeline.ErrCodeActionTypeNotFoundException) { + return nil + } + + if err != nil { + return diag.Errorf("deleting CodePipeline Custom Action Type (%s): %s", d.Id(), err) + } + + return nil +} + +const customActionTypeResourceIDSeparator = "/" + +func CustomActionTypeCreateResourceID(category, provider, version string) string { + parts := []string{category, provider, version} + id := strings.Join(parts, customActionTypeResourceIDSeparator) + + return id +} + +func CustomActionTypeParseResourceID(id string) (string, string, string, error) { + parts := strings.Split(id, customActionTypeResourceIDSeparator) + + if len(parts) == 3 && parts[0] != "" && parts[1] != "" && parts[2] != "" { + return parts[0], parts[1], parts[2], nil + } + + return "", "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected category%[2]sprovider%[2]sversion", id, customActionTypeResourceIDSeparator) +} + +func FindCustomActionTypeByThreePartKey(ctx context.Context, conn *codepipeline.CodePipeline, category, provider, version string) (*codepipeline.ActionType, error) { + input := &codepipeline.ListActionTypesInput{ + ActionOwnerFilter: aws.String(codepipeline.ActionOwnerCustom), + } + var output *codepipeline.ActionType + + err := conn.ListActionTypesPagesWithContext(ctx, input, func(page *codepipeline.ListActionTypesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.ActionTypes { + if v == nil || v.Id == nil { + continue + } + + if aws.StringValue(v.Id.Category) == category && aws.StringValue(v.Id.Provider) == provider && aws.StringValue(v.Id.Version) == version { + output = v + + return false + } + } + + return !lastPage + }) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, &resource.NotFoundError{ + LastRequest: input, + } + } + + return output, nil +} + +func expandActionConfigurationProperty(tfMap map[string]interface{}) *codepipeline.ActionConfigurationProperty { + if tfMap == nil { + return nil + } + + apiObject := &codepipeline.ActionConfigurationProperty{} + + if v, ok := tfMap["description"].(string); ok && v != "" { + apiObject.Description = aws.String(v) + } + + if v, ok := tfMap["key"].(bool); ok { + apiObject.Key = aws.Bool(v) + } + + if v, ok := tfMap["name"].(string); ok && v != "" { + apiObject.Name = aws.String(v) + } + + if v, ok := tfMap["queryable"].(bool); ok && v { + apiObject.Queryable = aws.Bool(v) + } + + if v, ok := tfMap["required"].(bool); ok { + apiObject.Required = aws.Bool(v) + } + + if v, ok := tfMap["secret"].(bool); ok { + apiObject.Secret = aws.Bool(v) + } + + if v, ok := tfMap["type"].(string); ok && v != "" { + apiObject.Type = aws.String(v) + } + + return apiObject +} + +func expandActionConfigurationProperties(tfList []interface{}) []*codepipeline.ActionConfigurationProperty { + if len(tfList) == 0 { + return nil + } + + var apiObjects []*codepipeline.ActionConfigurationProperty + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject := expandActionConfigurationProperty(tfMap) + + if apiObject == nil { + continue + } + + apiObjects = append(apiObjects, apiObject) + } + + return apiObjects +} + +func expandArtifactDetails(tfMap map[string]interface{}) *codepipeline.ArtifactDetails { + if tfMap == nil { + return nil + } + + apiObject := &codepipeline.ArtifactDetails{} + + if v, ok := tfMap["maximum_count"].(int); ok { + apiObject.MaximumCount = aws.Int64(int64(v)) + } + + if v, ok := tfMap["minimum_count"].(int); ok { + apiObject.MinimumCount = aws.Int64(int64(v)) + } + + return apiObject +} + +func expandActionTypeSettings(tfMap map[string]interface{}) *codepipeline.ActionTypeSettings { + if tfMap == nil { + return nil + } + + apiObject := &codepipeline.ActionTypeSettings{} + + if v, ok := tfMap["entity_url_template"].(string); ok && v != "" { + apiObject.EntityUrlTemplate = aws.String(v) + } + + if v, ok := tfMap["execution_url_template"].(string); ok && v != "" { + apiObject.ExecutionUrlTemplate = aws.String(v) + } + + if v, ok := tfMap["revision_url_template"].(string); ok && v != "" { + apiObject.RevisionUrlTemplate = aws.String(v) + } + + if v, ok := tfMap["third_party_configuration_url"].(string); ok && v != "" { + apiObject.ThirdPartyConfigurationUrl = aws.String(v) + } + + return apiObject +} + +func flattenActionConfigurationProperty(d *schema.ResourceData, i int, apiObject *codepipeline.ActionConfigurationProperty) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.Description; v != nil { + tfMap["description"] = aws.StringValue(v) + } + + if v := apiObject.Key; v != nil { + tfMap["key"] = aws.BoolValue(v) + } + + if v := apiObject.Name; v != nil { + tfMap["name"] = aws.StringValue(v) + } + + if v := apiObject.Queryable; v != nil { + tfMap["queryable"] = aws.BoolValue(v) + } + + if v := apiObject.Required; v != nil { + tfMap["required"] = aws.BoolValue(v) + } + + if v := apiObject.Secret; v != nil { + tfMap["secret"] = aws.BoolValue(v) + } + + if v := apiObject.Type; v != nil { + tfMap["type"] = aws.StringValue(v) + } else { + // The AWS API does not return Type. + key := fmt.Sprintf("configuration_property.%d.type", i) + tfMap["type"] = d.Get(key).(string) + } + + return tfMap +} + +func flattenActionConfigurationProperties(d *schema.ResourceData, apiObjects []*codepipeline.ActionConfigurationProperty) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for i, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, flattenActionConfigurationProperty(d, i, apiObject)) + } + + return tfList +} + +func flattenArtifactDetails(apiObject *codepipeline.ArtifactDetails) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.MaximumCount; v != nil { + tfMap["maximum_count"] = aws.Int64Value(v) + } + + if v := apiObject.MinimumCount; v != nil { + tfMap["minimum_count"] = aws.Int64Value(v) + } + + return tfMap +} + +func flattenActionTypeSettings(apiObject *codepipeline.ActionTypeSettings) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.EntityUrlTemplate; v != nil { + tfMap["entity_url_template"] = aws.StringValue(v) + } + + if v := apiObject.ExecutionUrlTemplate; v != nil { + tfMap["execution_url_template"] = aws.StringValue(v) + } + + if v := apiObject.RevisionUrlTemplate; v != nil { + tfMap["revision_url_template"] = aws.StringValue(v) + } + + if v := apiObject.ThirdPartyConfigurationUrl; v != nil { + tfMap["third_party_configuration_url"] = aws.StringValue(v) + } + + return tfMap +} diff --git a/internal/service/codepipeline/custom_action_type_test.go b/internal/service/codepipeline/custom_action_type_test.go new file mode 100644 index 00000000000..0d70f7f7818 --- /dev/null +++ b/internal/service/codepipeline/custom_action_type_test.go @@ -0,0 +1,377 @@ +package codepipeline_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/codepipeline" + "github.com/aws/aws-sdk-go/service/codestarconnections" + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfcodepipeline "github.com/hashicorp/terraform-provider-aws/internal/service/codepipeline" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func TestAccCodePipelineCustomActionType_basic(t *testing.T) { + var v codepipeline.ActionType + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_codepipeline_custom_action_type.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(codestarconnections.EndpointsID, t) + }, + ErrorCheck: acctest.ErrorCheck(t, codepipeline.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCustomActionTypeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCustomActionTypeConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckCustomActionTypeExists(resourceName, &v), + acctest.CheckResourceAttrRegionalARN(resourceName, "arn", "codepipeline", fmt.Sprintf("actiontype:Custom/Test/%s/1", rName)), + resource.TestCheckResourceAttr(resourceName, "category", "Test"), + resource.TestCheckResourceAttr(resourceName, "configuration_property.#", "0"), + resource.TestCheckResourceAttr(resourceName, "input_artifact_details.#", "1"), + resource.TestCheckResourceAttr(resourceName, "input_artifact_details.0.maximum_count", "5"), + resource.TestCheckResourceAttr(resourceName, "input_artifact_details.0.minimum_count", "0"), + resource.TestCheckResourceAttr(resourceName, "output_artifact_details.#", "1"), + resource.TestCheckResourceAttr(resourceName, "output_artifact_details.0.maximum_count", "4"), + resource.TestCheckResourceAttr(resourceName, "output_artifact_details.0.minimum_count", "1"), + resource.TestCheckResourceAttr(resourceName, "owner", "Custom"), + resource.TestCheckResourceAttr(resourceName, "provider_name", rName), + resource.TestCheckResourceAttr(resourceName, "settings.#", "0"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "version", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccCodePipelineCustomActionType_disappears(t *testing.T) { + var v codepipeline.ActionType + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_codepipeline_custom_action_type.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(codestarconnections.EndpointsID, t) + }, + ErrorCheck: acctest.ErrorCheck(t, codepipeline.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCustomActionTypeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCustomActionTypeConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckCustomActionTypeExists(resourceName, &v), + acctest.CheckResourceDisappears(acctest.Provider, tfcodepipeline.ResourceCustomActionType(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccCodePipelineCustomActionType_tags(t *testing.T) { + var v codepipeline.ActionType + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_codepipeline_custom_action_type.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(codestarconnections.EndpointsID, t) + }, + ErrorCheck: acctest.ErrorCheck(t, codepipeline.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCustomActionTypeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCustomActionTypeConfig_tags1(rName, "key1", "value1"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckCustomActionTypeExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccCustomActionTypeConfig_tags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckCustomActionTypeExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccCustomActionTypeConfig_tags1(rName, "key2", "value2"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckCustomActionTypeExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func TestAccCodePipelineCustomActionType_allAttributes(t *testing.T) { + var v codepipeline.ActionType + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_codepipeline_custom_action_type.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(codestarconnections.EndpointsID, t) + }, + ErrorCheck: acctest.ErrorCheck(t, codepipeline.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCustomActionTypeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCustomActionTypeConfig_allAttributes(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckCustomActionTypeExists(resourceName, &v), + acctest.CheckResourceAttrRegionalARN(resourceName, "arn", "codepipeline", fmt.Sprintf("actiontype:Custom/Test/%s/1", rName)), + resource.TestCheckResourceAttr(resourceName, "category", "Test"), + resource.TestCheckResourceAttr(resourceName, "configuration_property.#", "2"), + resource.TestCheckResourceAttr(resourceName, "configuration_property.0.description", ""), + resource.TestCheckResourceAttr(resourceName, "configuration_property.0.key", "true"), + resource.TestCheckResourceAttr(resourceName, "configuration_property.0.name", "pk"), + resource.TestCheckResourceAttr(resourceName, "configuration_property.0.queryable", "true"), + resource.TestCheckResourceAttr(resourceName, "configuration_property.0.required", "true"), + resource.TestCheckResourceAttr(resourceName, "configuration_property.0.secret", "false"), + resource.TestCheckResourceAttr(resourceName, "configuration_property.0.type", "Number"), + resource.TestCheckResourceAttr(resourceName, "configuration_property.1.description", "Date of birth"), + resource.TestCheckResourceAttr(resourceName, "configuration_property.1.key", "false"), + resource.TestCheckResourceAttr(resourceName, "configuration_property.1.name", "dob"), + resource.TestCheckResourceAttr(resourceName, "configuration_property.1.queryable", "false"), + resource.TestCheckResourceAttr(resourceName, "configuration_property.1.required", "false"), + resource.TestCheckResourceAttr(resourceName, "configuration_property.1.secret", "true"), + resource.TestCheckResourceAttr(resourceName, "configuration_property.1.type", "String"), + resource.TestCheckResourceAttr(resourceName, "input_artifact_details.#", "1"), + resource.TestCheckResourceAttr(resourceName, "input_artifact_details.0.maximum_count", "3"), + resource.TestCheckResourceAttr(resourceName, "input_artifact_details.0.minimum_count", "2"), + resource.TestCheckResourceAttr(resourceName, "output_artifact_details.#", "1"), + resource.TestCheckResourceAttr(resourceName, "output_artifact_details.0.maximum_count", "5"), + resource.TestCheckResourceAttr(resourceName, "output_artifact_details.0.minimum_count", "4"), + resource.TestCheckResourceAttr(resourceName, "owner", "Custom"), + resource.TestCheckResourceAttr(resourceName, "provider_name", rName), + resource.TestCheckResourceAttr(resourceName, "settings.#", "1"), + resource.TestCheckResourceAttr(resourceName, "settings.0.entity_url_template", "https://example.com/entity"), + resource.TestCheckResourceAttr(resourceName, "settings.0.execution_url_template", ""), + resource.TestCheckResourceAttr(resourceName, "settings.0.revision_url_template", "https://example.com/configuration"), + resource.TestCheckResourceAttr(resourceName, "settings.0.third_party_configuration_url", ""), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "version", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "configuration_property.0.type", + "configuration_property.1.type", + }, + }, + }, + }) +} + +func testAccCheckCustomActionTypeExists(n string, v *codepipeline.ActionType) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No CodePipeline Custom Action Type ID is set") + } + + category, provider, version, err := tfcodepipeline.CustomActionTypeParseResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).CodePipelineConn + + output, err := tfcodepipeline.FindCustomActionTypeByThreePartKey(context.Background(), conn, category, provider, version) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccCheckCustomActionTypeDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).CodePipelineConn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_codepipeline_custom_action_type" { + continue + } + + category, provider, version, err := tfcodepipeline.CustomActionTypeParseResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + _, err = tfcodepipeline.FindCustomActionTypeByThreePartKey(context.Background(), conn, category, provider, version) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("CodePipeline Custom Action Type %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccCustomActionTypeConfig_basic(rName string) string { + return fmt.Sprintf(` +resource "aws_codepipeline_custom_action_type" "test" { + category = "Test" + + input_artifact_details { + maximum_count = 5 + minimum_count = 0 + } + + output_artifact_details { + maximum_count = 4 + minimum_count = 1 + } + + provider_name = %[1]q + version = "1" +} +`, rName) +} + +func testAccCustomActionTypeConfig_tags1(rName, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_codepipeline_custom_action_type" "test" { + category = "Test" + + input_artifact_details { + maximum_count = 5 + minimum_count = 0 + } + + output_artifact_details { + maximum_count = 4 + minimum_count = 1 + } + + provider_name = %[1]q + version = "1" + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1) +} + +func testAccCustomActionTypeConfig_tags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_codepipeline_custom_action_type" "test" { + category = "Test" + + input_artifact_details { + maximum_count = 5 + minimum_count = 0 + } + + output_artifact_details { + maximum_count = 4 + minimum_count = 1 + } + + provider_name = %[1]q + version = "1" + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} + +func testAccCustomActionTypeConfig_allAttributes(rName string) string { + return fmt.Sprintf(` +resource "aws_codepipeline_custom_action_type" "test" { + category = "Test" + + configuration_property { + key = true + name = "pk" + queryable = true + required = true + secret = false + type = "Number" + } + + configuration_property { + description = "Date of birth" + key = false + name = "dob" + queryable = false + required = false + secret = true + type = "String" + } + + input_artifact_details { + maximum_count = 3 + minimum_count = 2 + } + + output_artifact_details { + maximum_count = 5 + minimum_count = 4 + } + + provider_name = %[1]q + version = "1" + + settings { + entity_url_template = "https://example.com/entity" + revision_url_template = "https://example.com/configuration" + } +} +`, rName) +} diff --git a/internal/service/codepipeline/sweep.go b/internal/service/codepipeline/sweep.go index 08696ae883c..1161ff68b95 100644 --- a/internal/service/codepipeline/sweep.go +++ b/internal/service/codepipeline/sweep.go @@ -9,7 +9,6 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/codepipeline" - "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/sweep" @@ -24,27 +23,23 @@ func init() { func sweepPipelines(region string) error { client, err := sweep.SharedRegionalSweepClient(region) - if err != nil { return fmt.Errorf("error getting client: %w", err) } - + input := &codepipeline.ListPipelinesInput{} conn := client.(*conns.AWSClient).CodePipelineConn sweepResources := make([]sweep.Sweepable, 0) - var errs *multierror.Error - - input := &codepipeline.ListPipelinesInput{} err = conn.ListPipelinesPages(input, func(page *codepipeline.ListPipelinesOutput, lastPage bool) bool { if page == nil { return !lastPage } - for _, pipeline := range page.Pipelines { - r := ResourceCodePipeline() + for _, v := range page.Pipelines { + r := ResourcePipeline() d := r.Data(nil) - d.SetId(aws.StringValue(pipeline.Name)) + d.SetId(aws.StringValue(v.Name)) sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) } @@ -52,18 +47,20 @@ func sweepPipelines(region string) error { return !lastPage }) - if err != nil { - errs = multierror.Append(errs, fmt.Errorf("error listing Codepipeline Pipeline for %s: %w", region, err)) + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping Codepipeline Pipeline sweep for %s: %s", region, err) + return nil } - if err := sweep.SweepOrchestrator(sweepResources); err != nil { - errs = multierror.Append(errs, fmt.Errorf("error sweeping Codepipeline Pipeline for %s: %w", region, err)) + if err != nil { + return fmt.Errorf("error listing Codepipeline Pipelines (%s): %w", region, err) } - if sweep.SkipSweepError(errs.ErrorOrNil()) { - log.Printf("[WARN] Skipping Codepipeline Pipeline sweep for %s: %s", region, errs) - return nil + err = sweep.SweepOrchestrator(sweepResources) + + if err != nil { + return fmt.Errorf("error sweeping Codepipeline Pipelines (%s): %w", region, err) } - return errs.ErrorOrNil() + return nil } diff --git a/internal/service/codepipeline/webhook_test.go b/internal/service/codepipeline/webhook_test.go index 33a5d944a33..235afd4bf69 100644 --- a/internal/service/codepipeline/webhook_test.go +++ b/internal/service/codepipeline/webhook_test.go @@ -31,7 +31,7 @@ func TestAccCodePipelineWebhook_basic(t *testing.T) { }, ErrorCheck: acctest.ErrorCheck(t, codepipeline.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: testAccCheckDestroy, + CheckDestroy: testAccCheckPipelineDestroy, Steps: []resource.TestStep{ { Config: testAccWebhookConfig_basic(rName, githubToken), @@ -101,7 +101,7 @@ func TestAccCodePipelineWebhook_ipAuth(t *testing.T) { }, ErrorCheck: acctest.ErrorCheck(t, codepipeline.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: testAccCheckDestroy, + CheckDestroy: testAccCheckPipelineDestroy, Steps: []resource.TestStep{ { Config: testAccWebhookConfig_ipAuth(rName, githubToken), @@ -136,7 +136,7 @@ func TestAccCodePipelineWebhook_unauthenticated(t *testing.T) { }, ErrorCheck: acctest.ErrorCheck(t, codepipeline.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: testAccCheckDestroy, + CheckDestroy: testAccCheckPipelineDestroy, Steps: []resource.TestStep{ { Config: testAccWebhookConfig_unauthenticated(rName, githubToken), @@ -169,7 +169,7 @@ func TestAccCodePipelineWebhook_tags(t *testing.T) { }, ErrorCheck: acctest.ErrorCheck(t, codepipeline.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: testAccCheckDestroy, + CheckDestroy: testAccCheckPipelineDestroy, Steps: []resource.TestStep{ { Config: testAccWebhookConfig_tags(rName, "tag1value", "tag2value", githubToken), @@ -233,7 +233,7 @@ func TestAccCodePipelineWebhook_disappears(t *testing.T) { }, ErrorCheck: acctest.ErrorCheck(t, codepipeline.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: testAccCheckDestroy, + CheckDestroy: testAccCheckPipelineDestroy, Steps: []resource.TestStep{ { Config: testAccWebhookConfig_basic(rName, githubToken), @@ -262,7 +262,7 @@ func TestAccCodePipelineWebhook_UpdateAuthentication_secretToken(t *testing.T) { }, ErrorCheck: acctest.ErrorCheck(t, codepipeline.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: testAccCheckDestroy, + CheckDestroy: testAccCheckPipelineDestroy, Steps: []resource.TestStep{ { Config: testAccWebhookConfig_basic(rName, githubToken), diff --git a/website/docs/r/codepipeline_custom_action_type.html.markdown b/website/docs/r/codepipeline_custom_action_type.html.markdown new file mode 100644 index 00000000000..2bcaf44a102 --- /dev/null +++ b/website/docs/r/codepipeline_custom_action_type.html.markdown @@ -0,0 +1,93 @@ +--- +subcategory: "CodePipeline" +layout: "aws" +page_title: "AWS: aws_codepipeline_custom_action_type" +description: |- + Provides a CodePipeline CustomActionType. +--- + +# Resource: aws_codepipeline_custom_action_type + +Provides a CodeDeploy CustomActionType + +## Example Usage + +```terraform +resource "aws_codepipeline_custom_action_type" "example" { + category = "Build" + + input_artifact_details { + maximum_count = 1 + minimum_count = 0 + } + + output_artifact_details { + maximum_count = 1 + minimum_count = 0 + } + + provider_name = "example" + version = "1" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `category` - (Required) The category of the custom action. Valid values: `Source`, `Build`, `Deploy`, `Test`, `Invoke`, `Approval` +* `configuration_property` - (Optional) The configuration properties for the custom action. Max 10 items. + +The `configuration_property` object supports the following: + +* `description` - (Optional) The description of the action configuration property. +* `key` - (Required) Whether the configuration property is a key. +* `name` - (Required) The name of the action configuration property. +* `queryable` - (Optional) Indicates that the property will be used in conjunction with PollForJobs. +* `required` - (Required) Whether the configuration property is a required value. +* `secret`- (Required) Whether the configuration property is secret. +* `type`- (Optional) The type of the configuration property. Valid values: `String`, `Number`, `Boolean` + +* `input_artifact_details` - (Required) The details of the input artifact for the action. + +The `input_artifact_details` object supports the following: + +* `maximum_count` - (Required) The maximum number of artifacts allowed for the action type. Min: 0, Max: 5 +* `minimum_count` - (Required) The minimum number of artifacts allowed for the action type. Min: 0, Max: 5 + +* `output_artifact_details` - (Required) The details of the output artifact of the action. + +The `output_artifact_details` object supports the following: + +* `maximum_count` - (Required) The maximum number of artifacts allowed for the action type. Min: 0, Max: 5 +* `minimum_count` - (Required) The minimum number of artifacts allowed for the action type. Min: 0, Max: 5 + +* `provider_name` - (Required) The provider of the service used in the custom action +* `settings` - (Optional) The settings for an action type. + +The `settings` object supports the following: + +* `entity_url_template` - (Optional) The URL returned to the AWS CodePipeline console that provides a deep link to the resources of the external system. +* `execution_url_template` - (Optional) The URL returned to the AWS CodePipeline console that contains a link to the top-level landing page for the external system. +* `revision_url_template` - (Optional) The URL returned to the AWS CodePipeline console that contains a link to the page where customers can update or change the configuration of the external action. +* `third_party_configuration_url` - (Optional) The URL of a sign-up page where users can sign up for an external service and perform initial configuration of the action provided by that service. + +* `tags` - (Optional) Map of tags to assign to this resource. 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. +* `version` - (Required) The version identifier of the custom action. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - Composed of category, provider and version. For example, `Build:terraform:1` +* `arn` - The action ARN. +* `owner` - The creator of the action being called. +* `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). + +## Import + +CodeDeploy CustomActionType can be imported using the `id`, e.g. + +``` +$ terraform import aws_codepipeline_custom_action_type.example Build:terraform:1 +```