Repository: aws-cloudformation/cfn-lint

Issue:
v1.10.0: Possible false-positive for `E3054: 'RequiresCompatibilities' is a required property`
### CloudFormation Lint Version

1.10.0

### What operating system are you using?

Windows 11

### Describe the bug

After updating to `1.10.0` i get the following error for `AWS::ECS::TaskDefinition`:

```plaintext
[cfn-lint] E3054: 'RequiresCompatibilities' is a required property
Registers a new task definition from the supplied ``family`` and ``containerDefinitions``.
Optionally, you can add data volumes to your containers with the ``volumes`` parameter.
For more information about task definition parameters and defaults, see [Amazon ECS Task Definitions]([https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_defintions.html\](vscode-file://vscode-app/d:/Programme/VSCode/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)) in the *Amazon Elastic Container Service Developer Guide*.

You can specify a role for your task with the ``taskRoleArn`` parameter.
When you specify a role for a task, its containers can then use the latest versions of the CLI or SDKs to make API requests to the AWS services that are specified in the policy that's associated with the role.
For more information, see [IAM Roles for Tasks]([https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html\](vscode-file://vscode-app/d:/Programme/VSCode/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)) in the *Amazon Elastic Container Service Developer Guide*.....
```

### Expected behavior

cfn-lint doesn't give that error, since i can deploy the stack.

### Reproduction template

```yaml
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      NetworkMode: awsvpc
      TaskRoleArn: !GetAtt TaskRole.Arn
      ExecutionRoleArn: !GetAtt ExecutionRole.Arn
      Cpu: 512
      Memory: 1024
      RuntimePlatform:
        CpuArchitecture: ARM64
      ContainerDefinitions:
        - Name: ContainerName
          Image: <IMAGE>
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-region: !Ref AWS::Region
              awslogs-group: !Ref LogGroup
              awslogs-stream-prefix: !Ref AWS::StackName
```


Discussion:
This rule crosses into the `AWS::ECS::Service` definition as well.  Can you provide that resource as well? We should be triggering this only when the service has a LaunchType of Fargate.

```yaml
ECSService:
    Type: AWS::ECS::Service
    Properties:
      LaunchType: FARGATE
```

as part of this [comment](https://github.com/aws-cloudformation/cfn-lint/issues/1547#issuecomment-729311374) and replicated through some testing.
Here you go:

```yaml
  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterSettings:
        - Name: containerInsights
          Value: enabled
      CapacityProviders:
        - FARGATE

  ECSService:
    Type: AWS::ECS::Service
    Properties:
      Cluster: !Ref ECSCluster
      TaskDefinition: !GetAtt ECSTaskDefinition.TaskDefinitionArn
      DesiredCount: 1
      LaunchType: FARGATE
      PropagateTags: SERVICE
      NetworkConfiguration:
        AwsvpcConfiguration:
          SecurityGroups:
            - !GetAtt SecurityGroup.GroupId
          Subnets:
            - <Subnet>
      EnableExecuteCommand: true

  ECSTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      NetworkMode: awsvpc
      TaskRoleArn: !GetAtt TaskRole.Arn
      ExecutionRoleArn: !GetAtt ExecutionRole.Arn
      Cpu: 512
      Memory: 1024
      RuntimePlatform:
        CpuArchitecture: ARM64
      ContainerDefinitions:
        - Name: ContainerName
          Image: <IMAGE>
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-region: !Ref AWS::Region
              awslogs-group: !Ref LogGroup
              awslogs-stream-prefix: !Ref AWS::StackName
```
According to https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-taskdefinition.html#cfn-ecs-taskdefinition-requirescompatibilities, you do not need `RequiresCompatibilities`:

```plaintext
RequiresCompatibilities
The task launch types the task definition was validated against.
The valid values are EC2, FARGATE, and EXTERNAL.
For more information, see Amazon ECS launch types in the Amazon Elastic Container Service Developer Guide.

Required: No

Type: Array of String

Update requires: Replacement
```
Looks like `NetworkMode: awsvpc` will drop the need of `RequiresCompatibilities: [FARGATE]`

This will succeed without `RequiresCompatibilities` specified.  Dropping `NetworkMode` property will again require the `RequiresCompatibilities`
```
ECSTaskDefinition:
    Type: 'AWS::ECS::TaskDefinition'
    Properties:
      NetworkMode: awsvpc
```

When removing NetworkMode you will get the error  `Invalid request provided: CreateService error: Task definition does not support launch_type FARGATE. `
This caveat only seems to apply to `NetworkMode: awsvpc` other network modes still require FARGATE to specified.
This PR will remove this error when NetworkMode is `awsvpc` or an object (Function to some possibly dynamic value)
Thank you!



Patch:
diff --git a/src/cfnlint/rules/resources/ecs/ServiceFargate.py b/src/cfnlint/rules/resources/ecs/ServiceFargate.py
index 1d03742e68..3de5ff02cb 100644
--- a/src/cfnlint/rules/resources/ecs/ServiceFargate.py
+++ b/src/cfnlint/rules/resources/ecs/ServiceFargate.py
@@ -77,26 +77,35 @@ def _get_task_definition_properties(
         )
         if not task_definition:
             return
-
         for capabilities, capabilities_validator in get_value_from_path(
             task_definition_validator,
             task_definition,
             path=deque(["Properties", "RequiresCompatibilities"]),
         ):
-            if capabilities is None:
-                yield capabilities, capabilities_validator
-                continue
-            if not isinstance(capabilities, list):
-                continue
-            for capibility, _ in get_value_from_path(
+            for network_mode, network_mode_validator in get_value_from_path(
                 capabilities_validator,
-                capabilities,
-                path=deque(["*"]),
+                task_definition,
+                path=deque(["Properties", "NetworkMode"]),
             ):
-                if isinstance(capibility, dict) or capibility == "FARGATE":
-                    break
-            else:
-                yield capabilities, capabilities_validator
+
+                if network_mode == "awsvpc" or network_mode_validator.is_type(
+                    network_mode, "object"
+                ):
+                    continue
+                if capabilities is None:
+                    yield capabilities, capabilities_validator
+                    continue
+                if not isinstance(capabilities, list):
+                    continue
+                for capibility, _ in get_value_from_path(
+                    network_mode_validator,
+                    capabilities,
+                    path=deque(["*"]),
+                ):
+                    if isinstance(capibility, dict) or capibility == "FARGATE":
+                        break
+                else:
+                    yield capabilities, capabilities_validator
 
     def validate(
         self, validator: Validator, _: Any, instance: Any, schema: dict[str, Any]


Test patch:
diff --git a/test/unit/rules/resources/ecs/test_service_fargate.py b/test/unit/rules/resources/ecs/test_service_fargate.py
index 5ea578ccde..5c62b8b705 100644
--- a/test/unit/rules/resources/ecs/test_service_fargate.py
+++ b/test/unit/rules/resources/ecs/test_service_fargate.py
@@ -315,6 +315,83 @@ def rule():
             deque(["Resources", "Service", "Properties"]),
             [],
         ),
+        (
+            {
+                "Resources": {
+                    "TaskDefinition": jsonpatch.apply_patch(
+                        dict(_task_definition),
+                        [
+                            {
+                                "op": "add",
+                                "path": "/Properties/NetworkMode",
+                                "value": "awsvpc",
+                            },
+                            {
+                                "op": "remove",
+                                "path": "/Properties/RequiresCompatibilities",
+                            },
+                        ],
+                    ),
+                    "Service": dict(_service),
+                },
+            },
+            deque(["Resources", "Service", "Properties"]),
+            [],
+        ),
+        (
+            {
+                "Parameters": {"MyNetworkMode": {"Type": "String"}},
+                "Resources": {
+                    "TaskDefinition": jsonpatch.apply_patch(
+                        dict(_task_definition),
+                        [
+                            {
+                                "op": "add",
+                                "path": "/Properties/NetworkMode",
+                                "value": {"Ref": "MyNetworkMode"},
+                            },
+                            {
+                                "op": "remove",
+                                "path": "/Properties/RequiresCompatibilities",
+                            },
+                        ],
+                    ),
+                    "Service": dict(_service),
+                },
+            },
+            deque(["Resources", "Service", "Properties"]),
+            [],
+        ),
+        (
+            {
+                "Resources": {
+                    "TaskDefinition": jsonpatch.apply_patch(
+                        dict(_task_definition),
+                        [
+                            {
+                                "op": "add",
+                                "path": "/Properties/NetworkMode",
+                                "value": "host",
+                            },
+                            {
+                                "op": "remove",
+                                "path": "/Properties/RequiresCompatibilities",
+                            },
+                        ],
+                    ),
+                    "Service": dict(_service),
+                },
+            },
+            deque(["Resources", "Service", "Properties"]),
+            [
+                ValidationError(
+                    ("'RequiresCompatibilities' is a required property"),
+                    validator="required",
+                    rule=ServiceFargate(),
+                    path_override=deque(["Resources", "TaskDefinition", "Properties"]),
+                )
+            ],
+        ),
     ],
     indirect=["template"],
 )