Repository: getmoto/moto
Pull request #5831 (2023-01-09 20:05:29)

Admin: Improve documentation
Closes #5789 

codecov-commenter: # [Codecov](https://codecov.io/gh/getmoto/moto/pull/5831?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#5831](https://codecov.io/gh/getmoto/moto/pull/5831?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (6074105) into [master](https://codecov.io/gh/getmoto/moto/commit/9288071edb5964bf32872271464a8ed44f5923e9?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (9288071) will **not change** coverage.
> The diff coverage is `n/a`.

```diff
@@           Coverage Diff           @@
##           master    #5831   +/-   ##
=======================================
  Coverage   96.18%   96.18%           
=======================================
  Files         753      753           
  Lines       73969    73969           
=======================================
  Hits        71150    71150           
  Misses       2819     2819           
```

| Flag | Coverage Δ | |
|---|---|---|
| servertests | `36.95% <ø> (ø)` | |
| unittests | `96.12% <ø> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#carryforward-flags-in-the-pull-request-comment) to find out more.


Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f31f91c1a..a5edeeecf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,7 @@ Moto Changelog
 
 4.1.0
 -----
-Docker Digest for 4.1.0: <autopopulateddigest>
+Docker Digest for 4.1.0: _sha256:1c38613f7273054650d08f3cb3ce118753296d08ffbfee8c96400c89a529c9ca_
 
     General:
         * Dropped support for Py 3.6
diff --git a/docs/docs/configuration/prettify_responses.rst b/docs/docs/configuration/prettify_responses.rst
index e592a380a..1c5049877 100644
--- a/docs/docs/configuration/prettify_responses.rst
+++ b/docs/docs/configuration/prettify_responses.rst
@@ -30,8 +30,8 @@ Prettified output:
     </DeleteLaunchTemplatesResponse>
 
 
-Configuration
-#################
+Enabling Pretty responses
+#########################
 
 As changing responses can interfere with some external tools, it is disabled by default.
 If you want to enable it, use environment variable:
diff --git a/docs/docs/configuration/recorder/index.rst b/docs/docs/configuration/recorder/index.rst
index 11976fd8f..c086fcdb9 100644
--- a/docs/docs/configuration/recorder/index.rst
+++ b/docs/docs/configuration/recorder/index.rst
@@ -60,8 +60,8 @@ Usage in ServerMode:
 Note that this feature records and replays the incoming HTTP request. Randomized data created by Moto, such as resource ID's, will not be stored as part of the log.
 
 
-Configuration
-##################
+Recorder Configuration
+########################
 
 The requests are stored in a file called `moto_recording`, in the directory that Python is run from. You can configure this location using the following environment variable:
 `MOTO_RECORDER_FILEPATH=/whatever/path/you/want`
diff --git a/docs/docs/contributing/installation.rst b/docs/docs/contributing/installation.rst
index 739251319..73eccfbc2 100644
--- a/docs/docs/contributing/installation.rst
+++ b/docs/docs/contributing/installation.rst
@@ -18,8 +18,8 @@ Checking out the code
 Contributing to Moto involves forking the project.
 GitHub has a handy guide explaining how to do this: https://docs.github.com/en/get-started/quickstart/contributing-to-projects
 
-Installing Moto
-=================
+Installing Moto locally
+========================
 
 It is recommended to work from some kind of virtual environment, i.e. `virtualenv`, to prevent cross-contamination with other projects.
 From within such a virtualenv, run the following command to install all required dependencies:
diff --git a/docs/docs/getting_started.rst b/docs/docs/getting_started.rst
index 4ceb64d8a..ecc01ef2f 100644
--- a/docs/docs/getting_started.rst
+++ b/docs/docs/getting_started.rst
@@ -217,6 +217,9 @@ You need to ensure that the mocks are actually in place.
         export AWS_SESSION_TOKEN='testing'
         export AWS_DEFAULT_REGION='us-east-1'
 
+ #. Do no embed credentials directly in your code. This is always considered bad practice, regardless of whether you use Moto. It also makes it impossible to configure fake credentials for testing purposes.
+
+
  #. **VERY IMPORTANT**: ensure that you have your mocks set up *BEFORE* your `boto3` client is established.
     This can typically happen if you import a module that has a `boto3` client instantiated outside of a function.
     See the pesky imports section below on how to work around this.
diff --git a/docs/docs/server_mode.rst b/docs/docs/server_mode.rst
index abe1a2613..5960eb2c7 100644
--- a/docs/docs/server_mode.rst
+++ b/docs/docs/server_mode.rst
@@ -131,7 +131,7 @@ In Scala:
 
 In Terraform:
 
-.. code-block::
+.. code-block:: bash
 
     provider "aws" {
         region                      = "us-east-1"
diff --git a/docs/docs/services/sagemaker.rst b/docs/docs/services/sagemaker.rst
index 4533cca92..a9e90a411 100644
--- a/docs/docs/services/sagemaker.rst
+++ b/docs/docs/services/sagemaker.rst
@@ -170,7 +170,7 @@ sagemaker
 - [ ] describe_monitoring_schedule
 - [ ] describe_notebook_instance
 - [X] describe_notebook_instance_lifecycle_config
-- [ ] describe_pipeline
+- [X] describe_pipeline
 - [ ] describe_pipeline_definition_for_execution
 - [ ] describe_pipeline_execution
 - [X] describe_processing_job
diff --git a/moto/__init__.py b/moto/__init__.py
index 0be8546e5..b45c3d2ca 100644
--- a/moto/__init__.py
+++ b/moto/__init__.py
@@ -191,7 +191,7 @@ mock_all = MockAll
 # logging.getLogger('boto').setLevel(logging.CRITICAL)
 
 __title__ = "moto"
-__version__ = "4.1.0.dev"
+__version__ = "4.1.1.dev"
 
 
 try:
diff --git a/moto/ec2/responses/launch_templates.py b/moto/ec2/responses/launch_templates.py
index d9797f77e..595f37f2f 100644
--- a/moto/ec2/responses/launch_templates.py
+++ b/moto/ec2/responses/launch_templates.py
@@ -226,7 +226,11 @@ class LaunchTemplates(EC2BaseResponse):
             ret_versions = []
             if versions:
                 for v in versions:
-                    ret_versions.append(template.get_version(int(v)))
+                    if str(v).lower() == "$latest" or "$default":
+                        tv = template.get_version(v)
+                    else:
+                        tv = template.get_version(int(v))
+                    ret_versions.append(tv)
             elif min_version:
                 if max_version:
                     vMax = max_version
diff --git a/moto/ecs/models.py b/moto/ecs/models.py
index c94887ad4..7374ce403 100644
--- a/moto/ecs/models.py
+++ b/moto/ecs/models.py
@@ -1143,6 +1143,7 @@ class EC2ContainerServiceBackend(BaseBackend):
         container_instances,
         overrides,
         started_by,
+        tags=None,
     ):
         cluster = self._get_cluster(cluster_str)
 
@@ -1169,6 +1170,7 @@ class EC2ContainerServiceBackend(BaseBackend):
                 backend=self,
                 overrides=overrides or {},
                 started_by=started_by or "",
+                tags=tags,
             )
             tasks.append(task)
             self.update_container_instance_resources(
@@ -1177,7 +1179,10 @@ class EC2ContainerServiceBackend(BaseBackend):
             self.tasks[cluster.name][task.task_arn] = task
         return tasks
 
-    def describe_tasks(self, cluster_str, tasks):
+    def describe_tasks(self, cluster_str, tasks, include=None):
+        """
+        Only include=TAGS is currently supported.
+        """
         self._get_cluster(cluster_str)
 
         if not tasks:
@@ -1192,6 +1197,11 @@ class EC2ContainerServiceBackend(BaseBackend):
                     or any(task_id in task for task in tasks)
                 ):
                     response.append(task)
+        if "TAGS" in (include or []):
+            return response
+
+        for task in response:
+            task.tags = []
         return response
 
     def list_tasks(
diff --git a/moto/ecs/responses.py b/moto/ecs/responses.py
index 40c1f86e3..5a4c38988 100644
--- a/moto/ecs/responses.py
+++ b/moto/ecs/responses.py
@@ -163,7 +163,8 @@ class EC2ContainerServiceResponse(BaseResponse):
     def describe_tasks(self):
         cluster = self._get_param("cluster", "default")
         tasks = self._get_param("tasks")
-        data = self.ecs_backend.describe_tasks(cluster, tasks)
+        include = self._get_param("include")
+        data = self.ecs_backend.describe_tasks(cluster, tasks, include)
         return json.dumps(
             {"tasks": [task.response_object for task in data], "failures": []}
         )
@@ -174,8 +175,14 @@ class EC2ContainerServiceResponse(BaseResponse):
         task_definition_str = self._get_param("taskDefinition")
         container_instances = self._get_param("containerInstances")
         started_by = self._get_param("startedBy")
+        tags = self._get_param("tags")
         tasks = self.ecs_backend.start_task(
-            cluster_str, task_definition_str, container_instances, overrides, started_by
+            cluster_str,
+            task_definition_str,
+            container_instances,
+            overrides,
+            started_by,
+            tags,
         )
         return json.dumps(
             {"tasks": [task.response_object for task in tasks], "failures": []}
diff --git a/moto/kms/responses.py b/moto/kms/responses.py
index e135b5eed..cf354d5c4 100644
--- a/moto/kms/responses.py
+++ b/moto/kms/responses.py
@@ -5,7 +5,7 @@ import re
 import warnings
 
 from moto.core.responses import BaseResponse
-from moto.kms.utils import RESERVED_ALIASES
+from moto.kms.utils import RESERVED_ALIASES, RESERVED_ALIASE_TARGET_KEY_IDS
 from .models import kms_backends, KmsBackend
 from .policy_validator import validate_policy
 from .exceptions import (
@@ -273,14 +273,16 @@ class KmsResponse(BaseResponse):
                         "TargetKeyId": target_key_id,
                     }
                 )
-        for reserved_alias in RESERVED_ALIASES:
+        for reserved_alias, target_key_id in RESERVED_ALIASE_TARGET_KEY_IDS.items():
             exsisting = [
                 a for a in response_aliases if a["AliasName"] == reserved_alias
             ]
             if not exsisting:
+                arn = f"arn:aws:kms:{region}:{self.current_account}:{reserved_alias}"
                 response_aliases.append(
                     {
-                        "AliasArn": f"arn:aws:kms:{region}:{self.current_account}:{reserved_alias}",
+                        "TargetKeyId": target_key_id,
+                        "AliasArn": arn,
                         "AliasName": reserved_alias,
                     }
                 )
diff --git a/moto/kms/utils.py b/moto/kms/utils.py
index 5d1e81546..15da5d9e9 100644
--- a/moto/kms/utils.py
+++ b/moto/kms/utils.py
@@ -25,22 +25,26 @@ HEADER_LEN = KEY_ID_LEN + IV_LEN + TAG_LEN
 CIPHERTEXT_HEADER_FORMAT = f">{KEY_ID_LEN}s{IV_LEN}s{TAG_LEN}s"
 Ciphertext = namedtuple("Ciphertext", ("key_id", "iv", "ciphertext", "tag"))
 
-RESERVED_ALIASES = [
-    "alias/aws/acm",
-    "alias/aws/dynamodb",
-    "alias/aws/ebs",
-    "alias/aws/elasticfilesystem",
-    "alias/aws/es",
-    "alias/aws/glue",
-    "alias/aws/kinesisvideo",
-    "alias/aws/lambda",
-    "alias/aws/rds",
-    "alias/aws/redshift",
-    "alias/aws/s3",
-    "alias/aws/secretsmanager",
-    "alias/aws/ssm",
-    "alias/aws/xray",
-]
+RESERVED_ALIASE_TARGET_KEY_IDS = {
+    # NOTE: These would technically differ across account, but in that they are
+    # out of customer control, testing that they are different would be redundant.
+    "alias/aws/acm": "4f58743d-e279-4214-9270-8cc28277958d",
+    "alias/aws/dynamodb": "7e6aa0ea-15a4-4e72-8b32-58e46e776888",
+    "alias/aws/ebs": "7adeb491-68c9-4a5b-86ec-a86ce5364094",
+    "alias/aws/elasticfilesystem": "0ef0f111-cdc8-4dda-b0bc-bf625bd5f154",
+    "alias/aws/es": "3c7c1880-c353-4cea-9866-d8bc12f05573",
+    "alias/aws/glue": "90fd783f-e582-4cc2-a207-672ee67f8d58",
+    "alias/aws/kinesisvideo": "7fd4bff3-6eb7-4283-8f11-a7e0a793a181",
+    "alias/aws/lambda": "ff9c4f27-2f29-4d9b-bf38-02f88b52a70c",
+    "alias/aws/rds": "f5f30938-abed-41a2-a0f6-5482d02a2489",
+    "alias/aws/redshift": "dcdae9aa-593a-4e0b-9153-37325591901f",
+    "alias/aws/s3": "8c3faf07-f43c-4d11-abdb-9183079214c7",
+    "alias/aws/secretsmanager": "fee5173a-3972-428e-ae73-cd4c2a371222",
+    "alias/aws/ssm": "cb3f6250-5078-48c0-a75f-0290bf47694e",
+    "alias/aws/xray": "e9b758eb-6230-4744-93d1-ad3b7d71f2f6",
+}
+
+RESERVED_ALIASES = list(RESERVED_ALIASE_TARGET_KEY_IDS.keys())
 
 
 def generate_key_id(multi_region=False):
diff --git a/moto/sagemaker/models.py b/moto/sagemaker/models.py
index 1fbf54bc5..515091d46 100644
--- a/moto/sagemaker/models.py
+++ b/moto/sagemaker/models.py
@@ -1,6 +1,7 @@
 import json
 import os
 from datetime import datetime
+
 from moto.core import BaseBackend, BackendDict, BaseModel, CloudFormationModel
 from moto.sagemaker import validators
 from moto.utilities.paginator import paginate
@@ -10,6 +11,7 @@ from .exceptions import (
     AWSValidationException,
     ResourceNotFound,
 )
+from .utils import load_pipeline_definition_from_s3, arn_formatter
 
 
 PAGINATION_MODEL = {
@@ -44,10 +46,6 @@ PAGINATION_MODEL = {
 }
 
 
-def arn_formatter(_type, _id, account_id, region_name):
-    return f"arn:aws:sagemaker:{region_name}:{account_id}:{_type}/{_id}"
-
-
 class BaseObject(BaseModel):
     def camelCase(self, key):
         words = []
@@ -86,24 +84,42 @@ class FakePipeline(BaseObject):
         account_id,
         region_name,
         parallelism_configuration,
-        pipeline_definition_s3_location,
     ):
         self.pipeline_name = pipeline_name
         self.pipeline_arn = arn_formatter(
             "pipeline", pipeline_name, account_id, region_name
         )
-        self.pipeline_display_name = pipeline_display_name
+        self.pipeline_display_name = pipeline_display_name or pipeline_name
         self.pipeline_definition = pipeline_definition
         self.pipeline_description = pipeline_description
         self.role_arn = role_arn
         self.tags = tags or []
         self.parallelism_configuration = parallelism_configuration
-        self.pipeline_definition_s3_location = pipeline_definition_s3_location
 
         now_string = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
         self.creation_time = now_string
         self.last_modified_time = now_string
-        self.last_execution_time = now_string
+        self.last_execution_time = None
+
+        self.pipeline_status = "Active"
+        fake_user_profile_name = "fake-user-profile-name"
+        fake_domain_id = "fake-domain-id"
+        fake_user_profile_arn = arn_formatter(
+            "user-profile",
+            f"{fake_domain_id}/{fake_user_profile_name}",
+            account_id,
+            region_name,
+        )
+        self.created_by = {
+            "UserProfileArn": fake_user_profile_arn,
+            "UserProfileName": fake_user_profile_name,
+            "DomainId": fake_domain_id,
+        }
+        self.last_modified_by = {
+            "UserProfileArn": fake_user_profile_arn,
+            "UserProfileName": fake_user_profile_name,
+            "DomainId": fake_domain_id,
+        }
 
 
 class FakeProcessingJob(BaseObject):
@@ -1758,6 +1774,28 @@ class SageMakerModelBackend(BaseBackend):
         tags,
         parallelism_configuration,
     ):
+        if not any([pipeline_definition, pipeline_definition_s3_location]):
+            raise ValidationError(
+                "An error occurred (ValidationException) when calling the CreatePipeline operation: Either "
+                "Pipeline Definition or Pipeline Definition S3 location should be provided"
+            )
+        if all([pipeline_definition, pipeline_definition_s3_location]):
+            raise ValidationError(
+                "An error occurred (ValidationException) when calling the CreatePipeline operation: "
+                "Both Pipeline Definition and Pipeline Definition S3 Location shouldn't be present"
+            )
+
+        if pipeline_name in self.pipelines:
+            raise ValidationError(
+                f"An error occurred (ValidationException) when calling the CreatePipeline operation: Pipeline names "
+                f"must be unique within an AWS account and region. Pipeline with name ({pipeline_name}) already exists."
+            )
+
+        if pipeline_definition_s3_location:
+            pipeline_definition = load_pipeline_definition_from_s3(
+                pipeline_definition_s3_location, self.account_id
+            )
+
         pipeline = FakePipeline(
             pipeline_name,
             pipeline_display_name,
@@ -1767,7 +1805,6 @@ class SageMakerModelBackend(BaseBackend):
             tags,
             self.account_id,
             self.region_name,
-            pipeline_definition_s3_location,
             parallelism_configuration,
         )
 
@@ -1799,28 +1836,59 @@ class SageMakerModelBackend(BaseBackend):
                 message=f"Could not find pipeline with name {pipeline_name}."
             )
 
-        provided_kwargs = set(kwargs.keys())
-        allowed_kwargs = {
-            "pipeline_display_name",
-            "pipeline_definition",
-            "pipeline_definition_s3_location",
-            "pipeline_description",
-            "role_arn",
-            "parallelism_configuration",
-        }
-        invalid_kwargs = provided_kwargs - allowed_kwargs
-
-        if invalid_kwargs:
-            raise TypeError(
-                f"update_pipeline got unexpected keyword arguments '{invalid_kwargs}'"
+        if all(
+            [
+                kwargs.get("pipeline_definition"),
+                kwargs.get("pipeline_definition_s3_location"),
+            ]
+        ):
+            raise ValidationError(
+                "An error occurred (ValidationException) when calling the UpdatePipeline operation: "
+                "Both Pipeline Definition and Pipeline Definition S3 Location shouldn't be present"
             )
 
         for attr_key, attr_value in kwargs.items():
             if attr_value:
+                if attr_key == "pipeline_definition_s3_location":
+                    self.pipelines[
+                        pipeline_name
+                    ].pipeline_definition = load_pipeline_definition_from_s3(
+                        attr_value, self.account_id
+                    )
+                    continue
                 setattr(self.pipelines[pipeline_name], attr_key, attr_value)
 
         return pipeline_arn
 
+    def describe_pipeline(
+        self,
+        pipeline_name,
+    ):
+        try:
+            pipeline = self.pipelines[pipeline_name]
+        except KeyError:
+            raise ValidationError(
+                message=f"Could not find pipeline with name {pipeline_name}."
+            )
+
+        response = {
+            "PipelineArn": pipeline.pipeline_arn,
+            "PipelineName": pipeline.pipeline_name,
+            "PipelineDisplayName": pipeline.pipeline_display_name,
+            "PipelineDescription": pipeline.pipeline_description,
+            "PipelineDefinition": pipeline.pipeline_definition,
+            "RoleArn": pipeline.role_arn,
+            "PipelineStatus": pipeline.pipeline_status,
+            "CreationTime": pipeline.creation_time,
+            "LastModifiedTime": pipeline.last_modified_time,
+            "LastRunTime": pipeline.last_execution_time,
+            "CreatedBy": pipeline.created_by,
+            "LastModifiedBy": pipeline.last_modified_by,
+            "ParallelismConfiguration": pipeline.parallelism_configuration,
+        }
+
+        return response
+
     def list_pipelines(
         self,
         pipeline_name_prefix,
diff --git a/moto/sagemaker/responses.py b/moto/sagemaker/responses.py
index 34b3154aa..3c33ce99a 100644
--- a/moto/sagemaker/responses.py
+++ b/moto/sagemaker/responses.py
@@ -465,6 +465,13 @@ class SageMakerResponse(BaseResponse):
         response = self.sagemaker_backend.list_associations(self.request_params)
         return 200, {}, json.dumps(response)
 
+    @amzn_request_id
+    def describe_pipeline(self):
+        response = self.sagemaker_backend.describe_pipeline(
+            self._get_param("PipelineName")
+        )
+        return 200, {}, json.dumps(response)
+
     @amzn_request_id
     def create_pipeline(self):
         pipeline = self.sagemaker_backend.create_pipeline(
diff --git a/moto/sagemaker/utils.py b/moto/sagemaker/utils.py
new file mode 100644
index 000000000..1a83177ff
--- /dev/null
+++ b/moto/sagemaker/utils.py
@@ -0,0 +1,15 @@
+from moto.s3.models import s3_backends
+import json
+
+
+def load_pipeline_definition_from_s3(pipeline_definition_s3_location, account_id):
+    s3_backend = s3_backends[account_id]["global"]
+    result = s3_backend.get_object(
+        bucket_name=pipeline_definition_s3_location["Bucket"],
+        key_name=pipeline_definition_s3_location["ObjectKey"],
+    )
+    return json.loads(result.value)
+
+
+def arn_formatter(_type, _id, account_id, region_name):
+    return f"arn:aws:sagemaker:{region_name}:{account_id}:{_type}/{_id}"


Test patch:
diff --git a/tests/test_ec2/test_launch_templates.py b/tests/test_ec2/test_launch_templates.py
index 1133ee229..738c0e96d 100644
--- a/tests/test_ec2/test_launch_templates.py
+++ b/tests/test_ec2/test_launch_templates.py
@@ -85,6 +85,22 @@ def test_describe_launch_template_versions():
     templ = resp["LaunchTemplateVersions"][0]["LaunchTemplateData"]
     templ.should.equal(template_data)
 
+    # test using $Latest version
+    resp = cli.describe_launch_template_versions(
+        LaunchTemplateName=template_name, Versions=["$Latest"]
+    )
+
+    templ = resp["LaunchTemplateVersions"][0]["LaunchTemplateData"]
+    templ.should.equal(template_data)
+
+    # test using $Default version
+    resp = cli.describe_launch_template_versions(
+        LaunchTemplateName=template_name, Versions=["$Default"]
+    )
+
+    templ = resp["LaunchTemplateVersions"][0]["LaunchTemplateData"]
+    templ.should.equal(template_data)
+
 
 @mock_ec2
 def test_describe_launch_template_versions_by_name_when_absent():
diff --git a/tests/test_ecs/test_ecs_boto3.py b/tests/test_ecs/test_ecs_boto3.py
index dc7127112..161212b01 100644
--- a/tests/test_ecs/test_ecs_boto3.py
+++ b/tests/test_ecs/test_ecs_boto3.py
@@ -1889,52 +1889,65 @@ def test_run_task_exceptions():
 @mock_ecs
 def test_start_task():
     client = boto3.client("ecs", region_name="us-east-1")
-    ec2 = boto3.resource("ec2", region_name="us-east-1")
-
     test_cluster_name = "test_ecs_cluster"
+    setup_ecs_cluster_with_ec2_instance(client, test_cluster_name)
 
-    _ = client.create_cluster(clusterName=test_cluster_name)
-
-    test_instance = ec2.create_instances(
-        ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1
-    )[0]
+    container_instances = client.list_container_instances(cluster=test_cluster_name)
+    container_instance_id = container_instances["containerInstanceArns"][0].split("/")[
+        -1
+    ]
 
-    instance_id_document = json.dumps(
-        ec2_utils.generate_instance_identity_document(test_instance)
+    response = client.start_task(
+        cluster="test_ecs_cluster",
+        taskDefinition="test_ecs_task",
+        overrides={},
+        containerInstances=[container_instance_id],
+        startedBy="moto",
     )
 
-    client.register_container_instance(
-        cluster=test_cluster_name, instanceIdentityDocument=instance_id_document
+    len(response["tasks"]).should.equal(1)
+    response["tasks"][0]["taskArn"].should.contain(
+        f"arn:aws:ecs:us-east-1:{ACCOUNT_ID}:task/"
+    )
+    response["tasks"][0]["clusterArn"].should.equal(
+        f"arn:aws:ecs:us-east-1:{ACCOUNT_ID}:cluster/test_ecs_cluster"
+    )
+    response["tasks"][0]["taskDefinitionArn"].should.equal(
+        f"arn:aws:ecs:us-east-1:{ACCOUNT_ID}:task-definition/test_ecs_task:1"
+    )
+    response["tasks"][0]["containerInstanceArn"].should.equal(
+        f"arn:aws:ecs:us-east-1:{ACCOUNT_ID}:container-instance/test_ecs_cluster/{container_instance_id}"
     )
+    response["tasks"][0]["tags"].should.equal(
+        [],
+    )
+    response["tasks"][0]["overrides"].should.equal({})
+    response["tasks"][0]["lastStatus"].should.equal("RUNNING")
+    response["tasks"][0]["desiredStatus"].should.equal("RUNNING")
+    response["tasks"][0]["startedBy"].should.equal("moto")
+    response["tasks"][0]["stoppedReason"].should.equal("")
+
+
+@mock_ec2
+@mock_ecs
+def test_start_task_with_tags():
+    client = boto3.client("ecs", region_name="us-east-1")
+    test_cluster_name = "test_ecs_cluster"
+    setup_ecs_cluster_with_ec2_instance(client, test_cluster_name)
 
     container_instances = client.list_container_instances(cluster=test_cluster_name)
     container_instance_id = container_instances["containerInstanceArns"][0].split("/")[
         -1
     ]
 
-    _ = client.register_task_definition(
-        family="test_ecs_task",
-        containerDefinitions=[
-            {
-                "name": "hello_world",
-                "image": "docker/hello-world:latest",
-                "cpu": 1024,
-                "memory": 400,
-                "essential": True,
-                "environment": [
-                    {"name": "AWS_ACCESS_KEY_ID", "value": "SOME_ACCESS_KEY"}
-                ],
-                "logConfiguration": {"logDriver": "json-file"},
-            }
-        ],
-    )
-
+    task_tags = [{"key": "Name", "value": "test_ecs_start_task"}]
     response = client.start_task(
         cluster="test_ecs_cluster",
         taskDefinition="test_ecs_task",
         overrides={},
         containerInstances=[container_instance_id],
         startedBy="moto",
+        tags=task_tags,
     )
 
     len(response["tasks"]).should.equal(1)
@@ -1950,6 +1963,9 @@ def test_start_task():
     response["tasks"][0]["containerInstanceArn"].should.equal(
         f"arn:aws:ecs:us-east-1:{ACCOUNT_ID}:container-instance/test_ecs_cluster/{container_instance_id}"
     )
+    response["tasks"][0]["tags"].should.equal(
+        task_tags,
+    )
     response["tasks"][0]["overrides"].should.equal({})
     response["tasks"][0]["lastStatus"].should.equal("RUNNING")
     response["tasks"][0]["desiredStatus"].should.equal("RUNNING")
@@ -2056,40 +2072,75 @@ def test_list_tasks_exceptions():
 @mock_ecs
 def test_describe_tasks():
     client = boto3.client("ecs", region_name="us-east-1")
-    ec2 = boto3.resource("ec2", region_name="us-east-1")
-
     test_cluster_name = "test_ecs_cluster"
+    setup_ecs_cluster_with_ec2_instance(client, test_cluster_name)
 
-    _ = client.create_cluster(clusterName=test_cluster_name)
+    tasks_arns = [
+        task["taskArn"]
+        for task in client.run_task(
+            cluster="test_ecs_cluster",
+            overrides={},
+            taskDefinition="test_ecs_task",
+            count=2,
+            startedBy="moto",
+        )["tasks"]
+    ]
+    response = client.describe_tasks(cluster="test_ecs_cluster", tasks=tasks_arns)
 
-    test_instance = ec2.create_instances(
-        ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1
-    )[0]
+    len(response["tasks"]).should.equal(2)
+    set(
+        [response["tasks"][0]["taskArn"], response["tasks"][1]["taskArn"]]
+    ).should.equal(set(tasks_arns))
 
-    instance_id_document = json.dumps(
-        ec2_utils.generate_instance_identity_document(test_instance)
+    # Test we can pass task ids instead of ARNs
+    response = client.describe_tasks(
+        cluster="test_ecs_cluster", tasks=[tasks_arns[0].split("/")[-1]]
     )
+    len(response["tasks"]).should.equal(1)
 
-    client.register_container_instance(
-        cluster=test_cluster_name, instanceIdentityDocument=instance_id_document
+
+@mock_ec2
+@mock_ecs
+def test_describe_tasks_empty_tags():
+    client = boto3.client("ecs", region_name="us-east-1")
+    test_cluster_name = "test_ecs_cluster"
+    setup_ecs_cluster_with_ec2_instance(client, test_cluster_name)
+
+    tasks_arns = [
+        task["taskArn"]
+        for task in client.run_task(
+            cluster="test_ecs_cluster",
+            overrides={},
+            taskDefinition="test_ecs_task",
+            count=2,
+            startedBy="moto",
+        )["tasks"]
+    ]
+    response = client.describe_tasks(
+        cluster="test_ecs_cluster", tasks=tasks_arns, include=["TAGS"]
     )
 
-    _ = client.register_task_definition(
-        family="test_ecs_task",
-        containerDefinitions=[
-            {
-                "name": "hello_world",
-                "image": "docker/hello-world:latest",
-                "cpu": 1024,
-                "memory": 400,
-                "essential": True,
-                "environment": [
-                    {"name": "AWS_ACCESS_KEY_ID", "value": "SOME_ACCESS_KEY"}
-                ],
-                "logConfiguration": {"logDriver": "json-file"},
-            }
-        ],
+    len(response["tasks"]).should.equal(2)
+    set(
+        [response["tasks"][0]["taskArn"], response["tasks"][1]["taskArn"]]
+    ).should.equal(set(tasks_arns))
+    response["tasks"][0]["tags"].should.equal([])
+
+    # Test we can pass task ids instead of ARNs
+    response = client.describe_tasks(
+        cluster="test_ecs_cluster", tasks=[tasks_arns[0].split("/")[-1]]
     )
+    len(response["tasks"]).should.equal(1)
+
+
+@mock_ec2
+@mock_ecs
+def test_describe_tasks_include_tags():
+    client = boto3.client("ecs", region_name="us-east-1")
+    test_cluster_name = "test_ecs_cluster"
+    setup_ecs_cluster_with_ec2_instance(client, test_cluster_name)
+
+    task_tags = [{"key": "Name", "value": "test_ecs_task"}]
     tasks_arns = [
         task["taskArn"]
         for task in client.run_task(
@@ -2098,14 +2149,18 @@ def test_describe_tasks():
             taskDefinition="test_ecs_task",
             count=2,
             startedBy="moto",
+            tags=task_tags,
         )["tasks"]
     ]
-    response = client.describe_tasks(cluster="test_ecs_cluster", tasks=tasks_arns)
+    response = client.describe_tasks(
+        cluster="test_ecs_cluster", tasks=tasks_arns, include=["TAGS"]
+    )
 
     len(response["tasks"]).should.equal(2)
     set(
         [response["tasks"][0]["taskArn"], response["tasks"][1]["taskArn"]]
     ).should.equal(set(tasks_arns))
+    response["tasks"][0]["tags"].should.equal(task_tags)
 
     # Test we can pass task ids instead of ARNs
     response = client.describe_tasks(
@@ -3721,3 +3776,35 @@ def setup_ecs(client, ec2):
     )
 
     return subnet, sg
+
+
+def setup_ecs_cluster_with_ec2_instance(client, test_cluster_name):
+    ec2 = boto3.resource("ec2", region_name="us-east-1")
+
+    _ = client.create_cluster(clusterName=test_cluster_name)
+    test_instance = ec2.create_instances(
+        ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1
+    )[0]
+    instance_id_document = json.dumps(
+        ec2_utils.generate_instance_identity_document(test_instance)
+    )
+    client.register_container_instance(
+        cluster=test_cluster_name, instanceIdentityDocument=instance_id_document
+    )
+
+    _ = client.register_task_definition(
+        family="test_ecs_task",
+        containerDefinitions=[
+            {
+                "name": "hello_world",
+                "image": "docker/hello-world:latest",
+                "cpu": 1024,
+                "memory": 400,
+                "essential": True,
+                "environment": [
+                    {"name": "AWS_ACCESS_KEY_ID", "value": "SOME_ACCESS_KEY"}
+                ],
+                "logConfiguration": {"logDriver": "json-file"},
+            }
+        ],
+    )
diff --git a/tests/test_kms/test_kms_boto3.py b/tests/test_kms/test_kms_boto3.py
index 6e1a4fcc5..f51b75129 100644
--- a/tests/test_kms/test_kms_boto3.py
+++ b/tests/test_kms/test_kms_boto3.py
@@ -275,13 +275,23 @@ def test_list_aliases():
     client = boto3.client("kms", region_name=region)
     create_simple_key(client)
 
+    default_alias_target_keys = {
+        "aws/ebs": "7adeb491-68c9-4a5b-86ec-a86ce5364094",
+        "aws/s3": "8c3faf07-f43c-4d11-abdb-9183079214c7",
+        "aws/redshift": "dcdae9aa-593a-4e0b-9153-37325591901f",
+        "aws/rds": "f5f30938-abed-41a2-a0f6-5482d02a2489",
+    }
+    default_alias_names = list(default_alias_target_keys.keys())
+
     aliases = client.list_aliases()["Aliases"]
     aliases.should.have.length_of(14)
-    default_alias_names = ["aws/ebs", "aws/s3", "aws/redshift", "aws/rds"]
     for name in default_alias_names:
         full_name = f"alias/{name}"
         arn = f"arn:aws:kms:{region}:{ACCOUNT_ID}:{full_name}"
-        aliases.should.contain({"AliasName": full_name, "AliasArn": arn})
+        target_key_id = default_alias_target_keys[name]
+        aliases.should.contain(
+            {"AliasName": full_name, "AliasArn": arn, "TargetKeyId": target_key_id}
+        )
 
 
 @pytest.mark.parametrize(
diff --git a/tests/test_sagemaker/test_sagemaker_pipeline.py b/tests/test_sagemaker/test_sagemaker_pipeline.py
index ea562646e..db76a42e5 100644
--- a/tests/test_sagemaker/test_sagemaker_pipeline.py
+++ b/tests/test_sagemaker/test_sagemaker_pipeline.py
@@ -1,18 +1,37 @@
-from moto import mock_sagemaker
+from contextlib import contextmanager
+from moto import mock_sagemaker, settings
 from time import sleep
 from datetime import datetime
 import boto3
 import botocore
+import json
 import pytest
+from unittest import SkipTest
 
+from moto.s3 import mock_s3
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
+from moto.sagemaker.utils import arn_formatter, load_pipeline_definition_from_s3
 
 FAKE_ROLE_ARN = f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole"
-TEST_REGION_NAME = "us-east-1"
+TEST_REGION_NAME = "us-west-1"
 
 
-def arn_formatter(_type, _id, account_id, region_name):
-    return f"arn:aws:sagemaker:{region_name}:{account_id}:{_type}/{_id}"
+@contextmanager
+def setup_s3_pipeline_definition(bucket_name, object_key, pipeline_definition):
+    client = boto3.client("s3")
+    client.create_bucket(
+        Bucket=bucket_name,
+        CreateBucketConfiguration={"LocationConstraint": TEST_REGION_NAME},
+    )
+    client.put_object(
+        Body=json.dumps(pipeline_definition),
+        Bucket=bucket_name,
+        Key=object_key,
+    )
+    yield
+
+    client.delete_object(Bucket=bucket_name, Key=object_key)
+    client.delete_bucket(Bucket=bucket_name)
 
 
 @pytest.fixture(name="sagemaker_client")
@@ -21,22 +40,46 @@ def fixture_sagemaker_client():
         yield boto3.client("sagemaker", region_name=TEST_REGION_NAME)
 
 
-def create_sagemaker_pipelines(sagemaker_client, pipeline_names, wait_seconds=0.0):
+def create_sagemaker_pipelines(sagemaker_client, pipelines, wait_seconds=0.0):
     responses = []
-    for pipeline_name in pipeline_names:
-        responses += sagemaker_client.create_pipeline(
-            PipelineName=pipeline_name,
-            RoleArn=FAKE_ROLE_ARN,
-        )
+    for pipeline in pipelines:
+        responses += sagemaker_client.create_pipeline(**pipeline)
         sleep(wait_seconds)
     return responses
 
 
+def test_load_pipeline_definition_from_s3():
+    if settings.TEST_SERVER_MODE:
+        raise SkipTest(
+            "Skipping test in server mode due to lack of access to s3_backend."
+        )
+
+    bucket_name = "some-bucket-1"
+    object_key = "some/object/key.json"
+    pipeline_definition = {"key": "value"}
+
+    with mock_s3():
+        with setup_s3_pipeline_definition(
+            bucket_name,
+            object_key,
+            pipeline_definition,
+        ):
+            observed_pipeline_definition = load_pipeline_definition_from_s3(
+                pipeline_definition_s3_location={
+                    "Bucket": bucket_name,
+                    "ObjectKey": object_key,
+                },
+                account_id=ACCOUNT_ID,
+            )
+    observed_pipeline_definition.should.equal(pipeline_definition)
+
+
 def test_create_pipeline(sagemaker_client):
     fake_pipeline_name = "MyPipelineName"
     response = sagemaker_client.create_pipeline(
         PipelineName=fake_pipeline_name,
         RoleArn=FAKE_ROLE_ARN,
+        PipelineDefinition=" ",
     )
     assert isinstance(response, dict)
     response["PipelineArn"].should.equal(
@@ -44,6 +87,48 @@ def test_create_pipeline(sagemaker_client):
     )
 
 
+@pytest.mark.parametrize(
+    "create_pipeline_kwargs",
+    [
+        {"PipelineName": "MyPipelineName", "RoleArn": FAKE_ROLE_ARN},
+        {"RoleArn": FAKE_ROLE_ARN, "PipelineDefinition": " "},
+        {"PipelineName": "MyPipelineName", "PipelineDefinition": " "},
+        {
+            "PipelineName": "MyPipelineName",
+            "RoleArn": FAKE_ROLE_ARN,
+            "PipelineDefinition": " ",
+            "PipelineDefinitionS3Location": {"key": "value"},
+        },
+    ],
+)
+def test_create_pipeline_invalid_required_kwargs(
+    sagemaker_client, create_pipeline_kwargs
+):
+    with pytest.raises(
+        (
+            botocore.exceptions.ParamValidationError,
+            botocore.exceptions.ClientError,
+        )
+    ):
+        _ = sagemaker_client.create_pipeline(
+            **create_pipeline_kwargs,
+        )
+
+
+def test_create_pipeline_duplicate_pipeline_name(sagemaker_client):
+    with pytest.raises(botocore.exceptions.ClientError):
+        _ = sagemaker_client.create_pipeline(
+            PipelineName="APipelineName",
+            RoleArn=FAKE_ROLE_ARN,
+            PipelineDefinition=" ",
+        )
+        _ = sagemaker_client.create_pipeline(
+            PipelineName="APipelineName",
+            RoleArn=FAKE_ROLE_ARN,
+            PipelineDefinition=" ",
+        )
+
+
 def test_list_pipelines_none(sagemaker_client):
     response = sagemaker_client.list_pipelines()
     assert isinstance(response, dict)
@@ -52,7 +137,15 @@ def test_list_pipelines_none(sagemaker_client):
 
 def test_list_pipelines_single(sagemaker_client):
     fake_pipeline_names = ["APipelineName"]
-    _ = create_sagemaker_pipelines(sagemaker_client, fake_pipeline_names)
+    pipelines = [
+        {
+            "PipelineName": fake_pipeline_names[0],
+            "RoleArn": FAKE_ROLE_ARN,
+            "PipelineDefinition": " ",
+        },
+    ]
+    _ = create_sagemaker_pipelines(sagemaker_client, pipelines)
+
     response = sagemaker_client.list_pipelines()
     response["PipelineSummaries"].should.have.length_of(1)
     response["PipelineSummaries"][0]["PipelineArn"].should.equal(
@@ -62,7 +155,16 @@ def test_list_pipelines_single(sagemaker_client):
 
 def test_list_pipelines_multiple(sagemaker_client):
     fake_pipeline_names = ["APipelineName", "BPipelineName"]
-    _ = create_sagemaker_pipelines(sagemaker_client, fake_pipeline_names)
+    pipelines = [
+        {
+            "PipelineName": fake_pipeline_name,
+            "RoleArn": FAKE_ROLE_ARN,
+            "PipelineDefinition": " ",
+        }
+        for fake_pipeline_name in fake_pipeline_names
+    ]
+    _ = create_sagemaker_pipelines(sagemaker_client, pipelines)
+
     response = sagemaker_client.list_pipelines(
         SortBy="Name",
         SortOrder="Ascending",
@@ -72,7 +174,16 @@ def test_list_pipelines_multiple(sagemaker_client):
 
 def test_list_pipelines_sort_name_ascending(sagemaker_client):
     fake_pipeline_names = ["APipelineName", "BPipelineName", "CPipelineName"]
-    _ = create_sagemaker_pipelines(sagemaker_client, fake_pipeline_names)
+    pipelines = [
+        {
+            "PipelineName": fake_pipeline_name,
+            "RoleArn": FAKE_ROLE_ARN,
+            "PipelineDefinition": " ",
+        }
+        for fake_pipeline_name in fake_pipeline_names
+    ]
+    _ = create_sagemaker_pipelines(sagemaker_client, pipelines)
+
     response = sagemaker_client.list_pipelines(
         SortBy="Name",
         SortOrder="Ascending",
@@ -90,7 +201,16 @@ def test_list_pipelines_sort_name_ascending(sagemaker_client):
 
 def test_list_pipelines_sort_creation_time_descending(sagemaker_client):
     fake_pipeline_names = ["APipelineName", "BPipelineName", "CPipelineName"]
-    _ = create_sagemaker_pipelines(sagemaker_client, fake_pipeline_names, 1)
+    pipelines = [
+        {
+            "PipelineName": fake_pipeline_name,
+            "RoleArn": FAKE_ROLE_ARN,
+            "PipelineDefinition": " ",
+        }
+        for fake_pipeline_name in fake_pipeline_names
+    ]
+    _ = create_sagemaker_pipelines(sagemaker_client, pipelines, 1.0)
+
     response = sagemaker_client.list_pipelines(
         SortBy="CreationTime",
         SortOrder="Descending",
@@ -108,14 +228,30 @@ def test_list_pipelines_sort_creation_time_descending(sagemaker_client):
 
 def test_list_pipelines_max_results(sagemaker_client):
     fake_pipeline_names = ["APipelineName", "BPipelineName", "CPipelineName"]
-    _ = create_sagemaker_pipelines(sagemaker_client, fake_pipeline_names, 0.0)
+    pipelines = [
+        {
+            "PipelineName": fake_pipeline_name,
+            "RoleArn": FAKE_ROLE_ARN,
+            "PipelineDefinition": " ",
+        }
+        for fake_pipeline_name in fake_pipeline_names
+    ]
+    _ = create_sagemaker_pipelines(sagemaker_client, pipelines)
+
     response = sagemaker_client.list_pipelines(MaxResults=2)
     response["PipelineSummaries"].should.have.length_of(2)
 
 
 def test_list_pipelines_next_token(sagemaker_client):
     fake_pipeline_names = ["APipelineName"]
-    _ = create_sagemaker_pipelines(sagemaker_client, fake_pipeline_names, 0.0)
+    pipelines = [
+        {
+            "PipelineName": fake_pipeline_names[0],
+            "RoleArn": FAKE_ROLE_ARN,
+            "PipelineDefinition": " ",
+        },
+    ]
+    _ = create_sagemaker_pipelines(sagemaker_client, pipelines)
 
     response = sagemaker_client.list_pipelines(NextToken="0")
     response["PipelineSummaries"].should.have.length_of(1)
@@ -123,7 +259,16 @@ def test_list_pipelines_next_token(sagemaker_client):
 
 def test_list_pipelines_pipeline_name_prefix(sagemaker_client):
     fake_pipeline_names = ["APipelineName", "BPipelineName", "CPipelineName"]
-    _ = create_sagemaker_pipelines(sagemaker_client, fake_pipeline_names, 0.0)
+    pipelines = [
+        {
+            "PipelineName": fake_pipeline_name,
+            "RoleArn": FAKE_ROLE_ARN,
+            "PipelineDefinition": " ",
+        }
+        for fake_pipeline_name in fake_pipeline_names
+    ]
+    _ = create_sagemaker_pipelines(sagemaker_client, pipelines)
+
     response = sagemaker_client.list_pipelines(PipelineNamePrefix="APipe")
     response["PipelineSummaries"].should.have.length_of(1)
     response["PipelineSummaries"][0]["PipelineName"].should.equal("APipelineName")
@@ -134,7 +279,15 @@ def test_list_pipelines_pipeline_name_prefix(sagemaker_client):
 
 def test_list_pipelines_created_after(sagemaker_client):
     fake_pipeline_names = ["APipelineName", "BPipelineName", "CPipelineName"]
-    _ = create_sagemaker_pipelines(sagemaker_client, fake_pipeline_names, 0.0)
+    pipelines = [
+        {
+            "PipelineName": fake_pipeline_name,
+            "RoleArn": FAKE_ROLE_ARN,
+            "PipelineDefinition": " ",
+        }
+        for fake_pipeline_name in fake_pipeline_names
+    ]
+    _ = create_sagemaker_pipelines(sagemaker_client, pipelines)
 
     created_after_str = "2099-12-31 23:59:59"
     response = sagemaker_client.list_pipelines(CreatedAfter=created_after_str)
@@ -151,7 +304,15 @@ def test_list_pipelines_created_after(sagemaker_client):
 
 def test_list_pipelines_created_before(sagemaker_client):
     fake_pipeline_names = ["APipelineName", "BPipelineName", "CPipelineName"]
-    _ = create_sagemaker_pipelines(sagemaker_client, fake_pipeline_names, 0.0)
+    pipelines = [
+        {
+            "PipelineName": fake_pipeline_name,
+            "RoleArn": FAKE_ROLE_ARN,
+            "PipelineDefinition": " ",
+        }
+        for fake_pipeline_name in fake_pipeline_names
+    ]
+    _ = create_sagemaker_pipelines(sagemaker_client, pipelines)
 
     created_before_str = "2000-12-31 23:59:59"
     response = sagemaker_client.list_pipelines(CreatedBefore=created_before_str)
@@ -182,7 +343,15 @@ def test_list_pipelines_invalid_values(sagemaker_client, list_pipelines_kwargs):
 
 def test_delete_pipeline_exists(sagemaker_client):
     fake_pipeline_names = ["APipelineName", "BPipelineName", "CPipelineName"]
-    _ = create_sagemaker_pipelines(sagemaker_client, fake_pipeline_names, 0.0)
+    pipelines = [
+        {
+            "PipelineName": fake_pipeline_name,
+            "RoleArn": FAKE_ROLE_ARN,
+            "PipelineDefinition": " ",
+        }
+        for fake_pipeline_name in fake_pipeline_names
+    ]
+    _ = create_sagemaker_pipelines(sagemaker_client, pipelines)
     pipeline_name_delete, pipeline_names_remain = (
         fake_pipeline_names[0],
         fake_pipeline_names[1:],
@@ -198,7 +367,7 @@ def test_delete_pipeline_exists(sagemaker_client):
     pipeline_names_exist = [
         pipeline["PipelineName"] for pipeline in response["PipelineSummaries"]
     ]
-    assert pipeline_names_remain == pipeline_names_exist
+    assert set(pipeline_names_remain) == set(pipeline_names_exist)
 
 
 def test_delete_pipeline_not_exists(sagemaker_client):
@@ -206,14 +375,36 @@ def test_delete_pipeline_not_exists(sagemaker_client):
         _ = sagemaker_client.delete_pipeline(PipelineName="some-pipeline-name")
 
 
-def test_update_pipeline(sagemaker_client):
+def test_update_pipeline_not_exists(sagemaker_client):
     with pytest.raises(botocore.exceptions.ClientError):
         _ = sagemaker_client.update_pipeline(PipelineName="some-pipeline-name")
 
 
+def test_update_pipeline_invalid_kwargs(sagemaker_client):
+    pipeline_name = "APipelineName"
+    pipeline = {
+        "PipelineName": pipeline_name,
+        "RoleArn": FAKE_ROLE_ARN,
+        "PipelineDefinition": " ",
+    }
+    _ = create_sagemaker_pipelines(sagemaker_client, [pipeline])
+
+    with pytest.raises(botocore.exceptions.ParamValidationError):
+        sagemaker_client.update_pipeline(
+            PipelineName=pipeline_name,
+            **{"InvalidKwarg": "some-value"},
+        )
+
+
 def test_update_pipeline_no_update(sagemaker_client):
     pipeline_name = "APipelineName"
-    _ = create_sagemaker_pipelines(sagemaker_client, [pipeline_name])
+    pipeline = {
+        "PipelineName": pipeline_name,
+        "RoleArn": FAKE_ROLE_ARN,
+        "PipelineDefinition": " ",
+    }
+    _ = create_sagemaker_pipelines(sagemaker_client, [pipeline])
+
     response = sagemaker_client.update_pipeline(PipelineName=pipeline_name)
     response["PipelineArn"].should.equal(
         arn_formatter("pipeline", pipeline_name, ACCOUNT_ID, TEST_REGION_NAME)
@@ -226,9 +417,14 @@ def test_update_pipeline_add_attribute(sagemaker_client):
     pipeline_name = "APipelineName"
     pipeline_display_name_update = "APipelineDisplayName"
 
-    _ = create_sagemaker_pipelines(sagemaker_client, [pipeline_name])
+    pipeline = {
+        "PipelineName": pipeline_name,
+        "RoleArn": FAKE_ROLE_ARN,
+        "PipelineDefinition": " ",
+    }
+    _ = create_sagemaker_pipelines(sagemaker_client, [pipeline])
     response = sagemaker_client.list_pipelines()
-    assert "PipelineDisplayName" not in response["PipelineSummaries"][0]
+    response["PipelineSummaries"][0]["PipelineDisplayName"].should.equal(pipeline_name)
 
     _ = sagemaker_client.update_pipeline(
         PipelineName=pipeline_name,
@@ -238,14 +434,19 @@ def test_update_pipeline_add_attribute(sagemaker_client):
     response["PipelineSummaries"][0]["PipelineDisplayName"].should.equal(
         pipeline_display_name_update
     )
-    response["PipelineSummaries"][0].should.have.length_of(7)
+    response["PipelineSummaries"][0].should.have.length_of(6)
 
 
 def test_update_pipeline_update_change_attribute(sagemaker_client):
     pipeline_name = "APipelineName"
     role_arn_update = f"{FAKE_ROLE_ARN}Test"
+    pipeline = {
+        "PipelineName": pipeline_name,
+        "RoleArn": FAKE_ROLE_ARN,
+        "PipelineDefinition": " ",
+    }
+    _ = create_sagemaker_pipelines(sagemaker_client, [pipeline])
 
-    _ = create_sagemaker_pipelines(sagemaker_client, [pipeline_name])
     _ = sagemaker_client.update_pipeline(
         PipelineName=pipeline_name,
         RoleArn=role_arn_update,
@@ -253,3 +454,36 @@ def test_update_pipeline_update_change_attribute(sagemaker_client):
     response = sagemaker_client.list_pipelines()
     response["PipelineSummaries"][0]["RoleArn"].should.equal(role_arn_update)
     response["PipelineSummaries"][0].should.have.length_of(6)
+
+
+def test_describe_pipeline_not_exists(sagemaker_client):
+    with pytest.raises(botocore.exceptions.ClientError):
+        _ = sagemaker_client.describe_pipeline(PipelineName="some-pipeline-name")
+
+
+@pytest.mark.parametrize(
+    "pipeline,expected_response_length",
+    [
+        (
+            {
+                "PipelineName": "APipelineName",
+                "RoleArn": FAKE_ROLE_ARN,
+                "PipelineDefinition": " ",
+            },
+            11,
+        ),
+        (
+            {
+                "PipelineName": "BPipelineName",
+                "RoleArn": FAKE_ROLE_ARN,
+                "PipelineDefinition": " ",
+                "PipelineDescription": "some pipeline description",
+            },
+            12,
+        ),
+    ],
+)
+def test_describe_pipeline_exists(sagemaker_client, pipeline, expected_response_length):
+    _ = create_sagemaker_pipelines(sagemaker_client, [pipeline])
+    response = sagemaker_client.describe_pipeline(PipelineName=pipeline["PipelineName"])
+    response.should.have.length_of(expected_response_length)