Repository: aws-cloudformation/cfn-lint

Issue:
Doesn't catch CodePipeline OutputArtifacts need to be uniquely named
cfn-lint 0.35.1

*Description of issue.*
The linter doesn't catch that CodePipeline `OutputArtifacts` need to be uniquely named.

Please provide as much information as possible:
* Template linting issues: 
  * Please provide a CloudFormation sample that generated the issue.

This template generates the error `UPDATE_FAILED | Output Artifact Bundle name must be unique within the pipeline. CreateOutput has been used more than once.`

<details>

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: The AWS CloudFormation template for this Serverless application
Resources:
  ServerlessDeploymentPipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      ArtifactStores:
        - Region: ca-central-1
          ArtifactStore:
            Type: S3
            Location: my-artifact-bucket
      Name: my-code-pipeline
      RestartExecutionOnUpdate: false
      RoleArn: arn:aws:iam::000000000000:role/root
      Stages:
        - Name: Source
          Actions:
            - Name: SourceAction
              ActionTypeId:
                Category: Source
                Owner: AWS
                Version: "1"
                Provider: S3
              OutputArtifacts:
                - Name: SourceArtifact
              Configuration:
                S3Bucket: my-source-bucket
                S3ObjectKey: source-item.zip
              RunOrder: 1
        - Name: DeployToEnvA
          Actions:
            - Name: CreateChangeSetEnvA
              Region: us-east-1
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: "1"
                Provider: CloudFormation
              InputArtifacts:
                - Name: SourceArtifact
              OutputArtifacts:
                - Name: CreateOutput
              Configuration:
                ActionMode: CHANGE_SET_REPLACE
                StackName: my-service-env-a
                Capabilities: CAPABILITY_NAMED_IAM
                RoleArn: arn:aws:iam::000000000000:role/root
                TemplatePath: SourceArtifact::env-a-us-east-1.json
                ChangeSetName: ChangeSet
              RunOrder: 1
              RoleArn: arn:aws:iam::000000000000:role/root
            - Name: CreateChangeSetEnvB
              Region: us-east-1
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: "1"
                Provider: CloudFormation
              InputArtifacts:
                - Name: SourceArtifact
              OutputArtifacts:
                - Name: CreateOutput
              Configuration:
                ActionMode: CHANGE_SET_REPLACE
                StackName: my-service-env-b
                Capabilities: CAPABILITY_NAMED_IAM
                RoleArn: arn:aws:iam::000000000000:role/root
                TemplatePath: SourceArtifact::env-b-us-east-1.json
                ChangeSetName: ChangeSet
              RunOrder: 1
              RoleArn: arn:aws:iam::000000000000:role/root

```

</details>

  * If present, please add links to the (official) documentation for clarification.
      - > Every output artifact in the pipeline must have a unique name.  

          [Source](https://docs.aws.amazon.com/codepipeline/latest/userguide/welcome-introducing-artifacts.html)
  * Validate if the issue still exists with the latest version of `cfn-lint` and/or the latest Spec files: :heavy_check_mark:  `0.35.1` is the latest version



Cfn-lint uses the [CloudFormation Resource Specifications](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-resource-specification.html) as the base to do validation. These files are included as part of the application version. Please update to the latest version of `cfn-lint` or update the spec files manually (`cfn-lint -u`)

:heavy_check_mark:  I have also tried after running `cfn-lint -u`


Discussion:

Resolving patch:
diff --git a/src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py b/src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py
--- a/src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py
+++ b/src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py
@@ -220,6 +220,35 @@ def check_names_unique(self, action, path, action_names):
 
         return matches
 
+    def check_artifact_names(self, action, path, artifact_names):
+        """Check that output artifact names are unique and inputs are from previous stage outputs."""
+        matches = []
+
+        input_artifacts = action.get('InputArtifacts')
+        if isinstance(input_artifacts, list):
+            for input_artifact in input_artifacts:
+                artifact_name = input_artifact.get('Name')
+                if isinstance(artifact_name, six.string_types):
+                    if not artifact_name in artifact_names:
+                        message = 'Every input artifact for an action must match the output artifact of an action earlier in the pipeline. ({name})'.format(
+                            name=artifact_name
+                        )
+                        matches.append(RuleMatch(path + ['InputArtifacts', 'Name'], message))
+
+        output_artifacts = action.get('OutputArtifacts')
+        if isinstance(output_artifacts, list):
+            for output_artifact in output_artifacts:
+                artifact_name = output_artifact.get('Name')
+                if isinstance(artifact_name, six.string_types):
+                    if artifact_name in artifact_names:
+                        message = 'Every output artifact in the pipeline must have a unique name. ({name})'.format(
+                            name=artifact_name
+                        )
+                        matches.append(RuleMatch(path + ['OutputArtifacts', 'Name'], message))
+                    artifact_names.add(artifact_name)
+
+        return matches
+
     def match(self, cfn):
         """Check that stage actions are set up properly."""
         matches = []
@@ -228,6 +257,7 @@ def match(self, cfn):
         for resource in resources:
             path = resource['Path']
             properties = resource['Value']
+            artifact_names = set()
 
             s_stages = properties.get_safe('Stages', path)
             for s_stage_v, s_stage_p in s_stages:
@@ -255,6 +285,8 @@ def match(self, cfn):
                                     l_i_a_action, 'InputArtifacts', full_path))
                                 matches.extend(self.check_artifact_counts(
                                     l_i_a_action, 'OutputArtifacts', full_path))
+                                matches.extend(self.check_artifact_names(
+                                    l_i_a_action, full_path, artifact_names))
                             except AttributeError as err:
                                 self.logger.debug('Got AttributeError. Should have been caught by generic linting. '
                                                   'Ignoring the error here: %s', str(err))


Test patch:
diff --git a/test/fixtures/templates/bad/resources/codepipeline/action_artifact_counts.yaml b/test/fixtures/templates/bad/resources/codepipeline/action_artifact_counts.yaml
--- a/test/fixtures/templates/bad/resources/codepipeline/action_artifact_counts.yaml
+++ b/test/fixtures/templates/bad/resources/codepipeline/action_artifact_counts.yaml
@@ -5,7 +5,7 @@ Resources:
     Properties:
       Name: Test-pipeline
       ArtifactStore:
-        Location: 'pipeline-bucket'
+        Location: "pipeline-bucket"
         Type: S3
       RoleArn: arn:aws:iam:::role/AWSCodePipelineRole
       Stages:
@@ -24,7 +24,21 @@ Resources:
                 Repo: cfn-python-lint
                 PollForSourceChanges: true
                 Branch: master
-                OAuthToken: 'secret-token'
+                OAuthToken: "secret-token"
+            - Name: Github2
+              ActionTypeId:
+                Category: Source
+                Owner: ThirdParty
+                Provider: GitHub
+                Version: "1"
+              OutputArtifacts:
+                - Name: MyApp2
+              Configuration:
+                Owner: aws-cloudformation
+                Repo: cfn-python-lint
+                PollForSourceChanges: true
+                Branch: master
+                OAuthToken: "secret-token"
             - Name: ECR
               ActionTypeId:
                 Category: Source
@@ -46,11 +60,11 @@ Resources:
               Configuration:
                 ProjectName: cfn-python-lint
               InputArtifacts:
-              - Name: MyApp
-              - Name: MyApp2  # No Longer an error
+                - Name: MyApp
+                - Name: MyApp2 # No Longer an error
               OutputArtifacts:
-              - Name: MyOutput1
-              - Name: MyOutput2  # No Longer an error
+                - Name: MyOutput2
+                - Name: MyOutput3 # No Longer an error
             - Name: JenkinsBuild
               ActionTypeId:
                 Category: Build
@@ -60,17 +74,17 @@ Resources:
               Configuration:
                 ProjectName: cfn-python-lint
               InputArtifacts:
-                - Name: MyInput1
-                - Name: MyInput2
-                - Name: MyInput3
-                - Name: MyInput4
-                - Name: MyInput5
-              OutputArtifacts:
+                - Name: MyApp
+                - Name: MyApp2
                 - Name: MyOutput1
                 - Name: MyOutput2
                 - Name: MyOutput3
+              OutputArtifacts:
                 - Name: MyOutput4
                 - Name: MyOutput5
+                - Name: MyOutput6
+                - Name: MyOutput7
+                - Name: MyOutput8
         - Name: Test
           Actions:
             - Name: Validate
@@ -89,12 +103,12 @@ Resources:
                 - Name: MyOutput5
                 - Name: MyOutput6 # Error
               OutputArtifacts:
-                - Name: MyOutput1
-                - Name: MyOutput2
-                - Name: MyOutput3
-                - Name: MyOutput4
-                - Name: MyOutput5
-                - Name: MyOutput6 # Error
+                - Name: MyOutput9
+                - Name: MyOutput10
+                - Name: MyOutput11
+                - Name: MyOutput12
+                - Name: MyOutput13
+                - Name: MyOutput14 # Error
             - Name: ValidateDeviceFarm
               ActionTypeId:
                 Category: Test
@@ -113,7 +127,7 @@ Resources:
               InputArtifacts:
                 - Name: MyOutput1
               OutputArtifacts:
-                - Name: MyOutput1
+                - Name: MyOutput15
             - Name: ValidateJenkins
               ActionTypeId:
                 Category: Test
@@ -123,17 +137,17 @@ Resources:
               Configuration:
                 ProjectName: cfn-python-lint
               InputArtifacts:
-                - Name: MyInput1
-                - Name: MyInput2
-                - Name: MyInput3
-                - Name: MyInput4
-                - Name: MyInput5
-              OutputArtifacts:
                 - Name: MyOutput1
                 - Name: MyOutput2
                 - Name: MyOutput3
                 - Name: MyOutput4
                 - Name: MyOutput5
+              OutputArtifacts:
+                - Name: MyOutput16
+                - Name: MyOutput17
+                - Name: MyOutput18
+                - Name: MyOutput19
+                - Name: MyOutput20
         - Name: Deploy
           Actions:
             - Name: MyDeploy
@@ -159,7 +173,7 @@ Resources:
                 ProductType: CLOUD_FORMATION_TEMPLATE
                 ProductId: port-99slboidonpew
               InputArtifacts:
-                - Name: MyInput1
+                - Name: MyOutput1
             - Name: MyDeployAlexaSkillsKit
               ActionTypeId:
                 Category: Deploy
@@ -172,8 +186,8 @@ Resources:
                 RefreshToken: 1234
                 SkillId: amzn1.ask.skill.22649d8f-0451-4b4b-9ed9-bfb6cEXAMPLE
               InputArtifacts:
-                - Name: MyInput1
-                - Name: MyInput2
+                - Name: MyOutput1
+                - Name: MyOutput2
         - Name: MyApprovalStage
           Actions:
             - Name: MyApprovalAction
@@ -183,9 +197,9 @@ Resources:
                 Version: "1"
                 Provider: Manual
               InputArtifacts:
-              - Name: MyApp # Error
+                - Name: MyApp # Error
               OutputArtifacts:
-              - Name: MyOutput1 # Error
+                - Name: MyOutput21 # Error
               Configuration:
                 NotificationArn: arn:aws:sns:us-east-2:80398EXAMPLE:MyApprovalTopic
                 ExternalEntityLink: http://example.com
diff --git a/test/fixtures/templates/bad/resources/codepipeline/input_artifact_not_exists.yaml b/test/fixtures/templates/bad/resources/codepipeline/input_artifact_not_exists.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/bad/resources/codepipeline/input_artifact_not_exists.yaml
@@ -0,0 +1,71 @@
+AWSTemplateFormatVersion: "2010-09-09"
+Description: The AWS CloudFormation template for this Serverless application
+Resources:
+  ServerlessDeploymentPipeline:
+    Type: AWS::CodePipeline::Pipeline
+    Properties:
+      ArtifactStores:
+        - Region: ca-central-1
+          ArtifactStore:
+            Type: S3
+            Location: my-artifact-bucket
+      Name: my-code-pipeline
+      RestartExecutionOnUpdate: false
+      RoleArn: arn:aws:iam::000000000000:role/root
+      Stages:
+        - Name: Source
+          Actions:
+            - Name: SourceAction
+              ActionTypeId:
+                Category: Source
+                Owner: AWS
+                Version: "1"
+                Provider: S3
+              OutputArtifacts:
+                - Name: SourceArtifact
+              Configuration:
+                S3Bucket: my-source-bucket
+                S3ObjectKey: source-item.zip
+              RunOrder: 1
+        - Name: DeployToEnvA
+          Actions:
+            - Name: CreateChangeSetEnvA
+              Region: us-east-1
+              ActionTypeId:
+                Category: Deploy
+                Owner: AWS
+                Version: "1"
+                Provider: CloudFormation
+              InputArtifacts:
+                - Name: SourceArtifactNonExistent
+              OutputArtifacts:
+                - Name: CreateOutput
+              Configuration:
+                ActionMode: CHANGE_SET_REPLACE
+                StackName: my-service-env-a
+                Capabilities: CAPABILITY_NAMED_IAM
+                RoleArn: arn:aws:iam::000000000000:role/root
+                TemplatePath: SourceArtifact::env-a-us-east-1.json
+                ChangeSetName: ChangeSet
+              RunOrder: 1
+              RoleArn: arn:aws:iam::000000000000:role/root
+            - Name: CreateChangeSetEnvB
+              Region: us-east-1
+              ActionTypeId:
+                Category: Deploy
+                Owner: AWS
+                Version: "1"
+                Provider: CloudFormation
+              InputArtifacts:
+                - Name: SourceArtifact
+              OutputArtifacts:
+                - Name: CreateOutput2
+              Configuration:
+                ActionMode: CHANGE_SET_REPLACE
+                StackName: my-service-env-b
+                Capabilities: CAPABILITY_NAMED_IAM
+                RoleArn: arn:aws:iam::000000000000:role/root
+                TemplatePath: SourceArtifact::env-b-us-east-1.json
+                ChangeSetName: ChangeSet
+              RunOrder: 1
+              RoleArn: arn:aws:iam::000000000000:role/root
diff --git a/test/fixtures/templates/bad/resources/codepipeline/output_artifact_non_unique.yaml b/test/fixtures/templates/bad/resources/codepipeline/output_artifact_non_unique.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/bad/resources/codepipeline/output_artifact_non_unique.yaml
@@ -0,0 +1,71 @@
+AWSTemplateFormatVersion: "2010-09-09"
+Description: The AWS CloudFormation template for this Serverless application
+Resources:
+  ServerlessDeploymentPipeline:
+    Type: AWS::CodePipeline::Pipeline
+    Properties:
+      ArtifactStores:
+        - Region: ca-central-1
+          ArtifactStore:
+            Type: S3
+            Location: my-artifact-bucket
+      Name: my-code-pipeline
+      RestartExecutionOnUpdate: false
+      RoleArn: arn:aws:iam::000000000000:role/root
+      Stages:
+        - Name: Source
+          Actions:
+            - Name: SourceAction
+              ActionTypeId:
+                Category: Source
+                Owner: AWS
+                Version: "1"
+                Provider: S3
+              OutputArtifacts:
+                - Name: SourceArtifact
+              Configuration:
+                S3Bucket: my-source-bucket
+                S3ObjectKey: source-item.zip
+              RunOrder: 1
+        - Name: DeployToEnvA
+          Actions:
+            - Name: CreateChangeSetEnvA
+              Region: us-east-1
+              ActionTypeId:
+                Category: Deploy
+                Owner: AWS
+                Version: "1"
+                Provider: CloudFormation
+              InputArtifacts:
+                - Name: SourceArtifact
+              OutputArtifacts:
+                - Name: CreateOutput
+              Configuration:
+                ActionMode: CHANGE_SET_REPLACE
+                StackName: my-service-env-a
+                Capabilities: CAPABILITY_NAMED_IAM
+                RoleArn: arn:aws:iam::000000000000:role/root
+                TemplatePath: SourceArtifact::env-a-us-east-1.json
+                ChangeSetName: ChangeSet
+              RunOrder: 1
+              RoleArn: arn:aws:iam::000000000000:role/root
+            - Name: CreateChangeSetEnvB
+              Region: us-east-1
+              ActionTypeId:
+                Category: Deploy
+                Owner: AWS
+                Version: "1"
+                Provider: CloudFormation
+              InputArtifacts:
+                - Name: SourceArtifact
+              OutputArtifacts:
+                - Name: CreateOutput
+              Configuration:
+                ActionMode: CHANGE_SET_REPLACE
+                StackName: my-service-env-b
+                Capabilities: CAPABILITY_NAMED_IAM
+                RoleArn: arn:aws:iam::000000000000:role/root
+                TemplatePath: SourceArtifact::env-b-us-east-1.json
+                ChangeSetName: ChangeSet
+              RunOrder: 1
+              RoleArn: arn:aws:iam::000000000000:role/root
diff --git a/test/unit/rules/resources/codepipeline/test_stageactions.py b/test/unit/rules/resources/codepipeline/test_stageactions.py
--- a/test/unit/rules/resources/codepipeline/test_stageactions.py
+++ b/test/unit/rules/resources/codepipeline/test_stageactions.py
@@ -35,3 +35,13 @@ def test_file_non_unique(self):
         """Test failure"""
         self.helper_file_negative(
             'test/fixtures/templates/bad/resources/codepipeline/action_non_unique.yaml', 1)
+
+    def test_output_artifact_names_unique(self):
+        """Test failure"""
+        self.helper_file_negative(
+            'test/fixtures/templates/bad/resources/codepipeline/output_artifact_non_unique.yaml', 1)
+
+    def test_input_artifact_names_exist(self):
+        """Test failure"""
+        self.helper_file_negative(
+            'test/fixtures/templates/bad/resources/codepipeline/input_artifact_not_exists.yaml', 1)
\ No newline at end of file