Merge pull request #2644 from gruebel/add-codepipeline-fix
CodePipeline - implement CRUD endpoints
This commit is contained in:
commit
b8a1f85285
File diff suppressed because it is too large
Load Diff
@ -9,6 +9,7 @@ from .batch import mock_batch # noqa
|
|||||||
from .cloudformation import mock_cloudformation # noqa
|
from .cloudformation import mock_cloudformation # noqa
|
||||||
from .cloudformation import mock_cloudformation_deprecated # noqa
|
from .cloudformation import mock_cloudformation_deprecated # noqa
|
||||||
from .cloudwatch import mock_cloudwatch, mock_cloudwatch_deprecated # noqa
|
from .cloudwatch import mock_cloudwatch, mock_cloudwatch_deprecated # noqa
|
||||||
|
from .codepipeline import mock_codepipeline # noqa
|
||||||
from .cognitoidentity import mock_cognitoidentity # noqa
|
from .cognitoidentity import mock_cognitoidentity # noqa
|
||||||
from .cognitoidentity import mock_cognitoidentity_deprecated # noqa
|
from .cognitoidentity import mock_cognitoidentity_deprecated # noqa
|
||||||
from .cognitoidp import mock_cognitoidp, mock_cognitoidp_deprecated # noqa
|
from .cognitoidp import mock_cognitoidp, mock_cognitoidp_deprecated # noqa
|
||||||
|
@ -8,6 +8,7 @@ from moto.awslambda import lambda_backends
|
|||||||
from moto.batch import batch_backends
|
from moto.batch import batch_backends
|
||||||
from moto.cloudformation import cloudformation_backends
|
from moto.cloudformation import cloudformation_backends
|
||||||
from moto.cloudwatch import cloudwatch_backends
|
from moto.cloudwatch import cloudwatch_backends
|
||||||
|
from moto.codepipeline import codepipeline_backends
|
||||||
from moto.cognitoidentity import cognitoidentity_backends
|
from moto.cognitoidentity import cognitoidentity_backends
|
||||||
from moto.cognitoidp import cognitoidp_backends
|
from moto.cognitoidp import cognitoidp_backends
|
||||||
from moto.config import config_backends
|
from moto.config import config_backends
|
||||||
@ -60,6 +61,7 @@ BACKENDS = {
|
|||||||
"batch": batch_backends,
|
"batch": batch_backends,
|
||||||
"cloudformation": cloudformation_backends,
|
"cloudformation": cloudformation_backends,
|
||||||
"cloudwatch": cloudwatch_backends,
|
"cloudwatch": cloudwatch_backends,
|
||||||
|
"codepipeline": codepipeline_backends,
|
||||||
"cognito-identity": cognitoidentity_backends,
|
"cognito-identity": cognitoidentity_backends,
|
||||||
"cognito-idp": cognitoidp_backends,
|
"cognito-idp": cognitoidp_backends,
|
||||||
"config": config_backends,
|
"config": config_backends,
|
||||||
|
4
moto/codepipeline/__init__.py
Normal file
4
moto/codepipeline/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from .models import codepipeline_backends
|
||||||
|
from ..core.models import base_decorator
|
||||||
|
|
||||||
|
mock_codepipeline = base_decorator(codepipeline_backends)
|
28
moto/codepipeline/exceptions.py
Normal file
28
moto/codepipeline/exceptions.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from moto.core.exceptions import JsonRESTError
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidStructureException(JsonRESTError):
|
||||||
|
code = 400
|
||||||
|
|
||||||
|
def __init__(self, message):
|
||||||
|
super(InvalidStructureException, self).__init__(
|
||||||
|
"InvalidStructureException", message
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PipelineNotFoundException(JsonRESTError):
|
||||||
|
code = 400
|
||||||
|
|
||||||
|
def __init__(self, message):
|
||||||
|
super(PipelineNotFoundException, self).__init__(
|
||||||
|
"PipelineNotFoundException", message
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceNotFoundException(JsonRESTError):
|
||||||
|
code = 400
|
||||||
|
|
||||||
|
def __init__(self, message):
|
||||||
|
super(ResourceNotFoundException, self).__init__(
|
||||||
|
"ResourceNotFoundException", message
|
||||||
|
)
|
151
moto/codepipeline/models.py
Normal file
151
moto/codepipeline/models.py
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from boto3 import Session
|
||||||
|
from moto.core.utils import iso_8601_datetime_with_milliseconds
|
||||||
|
|
||||||
|
from moto.iam.exceptions import IAMNotFoundException
|
||||||
|
|
||||||
|
from moto.iam import iam_backends
|
||||||
|
|
||||||
|
from moto.codepipeline.exceptions import (
|
||||||
|
InvalidStructureException,
|
||||||
|
PipelineNotFoundException,
|
||||||
|
ResourceNotFoundException,
|
||||||
|
)
|
||||||
|
from moto.core import BaseBackend, BaseModel
|
||||||
|
|
||||||
|
from moto.iam.models import ACCOUNT_ID
|
||||||
|
|
||||||
|
|
||||||
|
class CodePipeline(BaseModel):
|
||||||
|
def __init__(self, region, pipeline):
|
||||||
|
# the version number for a new pipeline is always 1
|
||||||
|
pipeline["version"] = 1
|
||||||
|
|
||||||
|
self.pipeline = self.add_default_values(pipeline)
|
||||||
|
self.tags = {}
|
||||||
|
|
||||||
|
self._arn = "arn:aws:codepipeline:{0}:{1}:{2}".format(
|
||||||
|
region, ACCOUNT_ID, pipeline["name"]
|
||||||
|
)
|
||||||
|
self._created = datetime.utcnow()
|
||||||
|
self._updated = datetime.utcnow()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def metadata(self):
|
||||||
|
return {
|
||||||
|
"pipelineArn": self._arn,
|
||||||
|
"created": iso_8601_datetime_with_milliseconds(self._created),
|
||||||
|
"updated": iso_8601_datetime_with_milliseconds(self._updated),
|
||||||
|
}
|
||||||
|
|
||||||
|
def add_default_values(self, pipeline):
|
||||||
|
for stage in pipeline["stages"]:
|
||||||
|
for action in stage["actions"]:
|
||||||
|
if "runOrder" not in action:
|
||||||
|
action["runOrder"] = 1
|
||||||
|
if "configuration" not in action:
|
||||||
|
action["configuration"] = {}
|
||||||
|
if "outputArtifacts" not in action:
|
||||||
|
action["outputArtifacts"] = []
|
||||||
|
if "inputArtifacts" not in action:
|
||||||
|
action["inputArtifacts"] = []
|
||||||
|
|
||||||
|
return pipeline
|
||||||
|
|
||||||
|
|
||||||
|
class CodePipelineBackend(BaseBackend):
|
||||||
|
def __init__(self):
|
||||||
|
self.pipelines = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def iam_backend(self):
|
||||||
|
return iam_backends["global"]
|
||||||
|
|
||||||
|
def create_pipeline(self, region, pipeline, tags):
|
||||||
|
if pipeline["name"] in self.pipelines:
|
||||||
|
raise InvalidStructureException(
|
||||||
|
"A pipeline with the name '{0}' already exists in account '{1}'".format(
|
||||||
|
pipeline["name"], ACCOUNT_ID
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
role = self.iam_backend.get_role_by_arn(pipeline["roleArn"])
|
||||||
|
service_principal = json.loads(role.assume_role_policy_document)[
|
||||||
|
"Statement"
|
||||||
|
][0]["Principal"]["Service"]
|
||||||
|
if "codepipeline.amazonaws.com" not in service_principal:
|
||||||
|
raise IAMNotFoundException("")
|
||||||
|
except IAMNotFoundException:
|
||||||
|
raise InvalidStructureException(
|
||||||
|
"CodePipeline is not authorized to perform AssumeRole on role {}".format(
|
||||||
|
pipeline["roleArn"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(pipeline["stages"]) < 2:
|
||||||
|
raise InvalidStructureException(
|
||||||
|
"Pipeline has only 1 stage(s). There should be a minimum of 2 stages in a pipeline"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.pipelines[pipeline["name"]] = CodePipeline(region, pipeline)
|
||||||
|
|
||||||
|
if tags:
|
||||||
|
new_tags = {tag["key"]: tag["value"] for tag in tags}
|
||||||
|
self.pipelines[pipeline["name"]].tags.update(new_tags)
|
||||||
|
|
||||||
|
return pipeline, tags
|
||||||
|
|
||||||
|
def get_pipeline(self, name):
|
||||||
|
codepipeline = self.pipelines.get(name)
|
||||||
|
|
||||||
|
if not codepipeline:
|
||||||
|
raise PipelineNotFoundException(
|
||||||
|
"Account '{0}' does not have a pipeline with name '{1}'".format(
|
||||||
|
ACCOUNT_ID, name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return codepipeline.pipeline, codepipeline.metadata
|
||||||
|
|
||||||
|
def update_pipeline(self, pipeline):
|
||||||
|
codepipeline = self.pipelines.get(pipeline["name"])
|
||||||
|
|
||||||
|
if not codepipeline:
|
||||||
|
raise ResourceNotFoundException(
|
||||||
|
"The account with id '{0}' does not include a pipeline with the name '{1}'".format(
|
||||||
|
ACCOUNT_ID, pipeline["name"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# version number is auto incremented
|
||||||
|
pipeline["version"] = codepipeline.pipeline["version"] + 1
|
||||||
|
codepipeline._updated = datetime.utcnow()
|
||||||
|
codepipeline.pipeline = codepipeline.add_default_values(pipeline)
|
||||||
|
|
||||||
|
return codepipeline.pipeline
|
||||||
|
|
||||||
|
def list_pipelines(self):
|
||||||
|
pipelines = []
|
||||||
|
|
||||||
|
for name, codepipeline in self.pipelines.items():
|
||||||
|
pipelines.append(
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"version": codepipeline.pipeline["version"],
|
||||||
|
"created": codepipeline.metadata["created"],
|
||||||
|
"updated": codepipeline.metadata["updated"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return sorted(pipelines, key=lambda i: i["name"])
|
||||||
|
|
||||||
|
def delete_pipeline(self, name):
|
||||||
|
self.pipelines.pop(name, None)
|
||||||
|
|
||||||
|
|
||||||
|
codepipeline_backends = {}
|
||||||
|
for region in Session().get_available_regions("codepipeline"):
|
||||||
|
codepipeline_backends[region] = CodePipelineBackend()
|
41
moto/codepipeline/responses.py
Normal file
41
moto/codepipeline/responses.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from moto.core.responses import BaseResponse
|
||||||
|
from .models import codepipeline_backends
|
||||||
|
|
||||||
|
|
||||||
|
class CodePipelineResponse(BaseResponse):
|
||||||
|
@property
|
||||||
|
def codepipeline_backend(self):
|
||||||
|
return codepipeline_backends[self.region]
|
||||||
|
|
||||||
|
def create_pipeline(self):
|
||||||
|
pipeline, tags = self.codepipeline_backend.create_pipeline(
|
||||||
|
self.region, self._get_param("pipeline"), self._get_param("tags")
|
||||||
|
)
|
||||||
|
|
||||||
|
return json.dumps({"pipeline": pipeline, "tags": tags})
|
||||||
|
|
||||||
|
def get_pipeline(self):
|
||||||
|
pipeline, metadata = self.codepipeline_backend.get_pipeline(
|
||||||
|
self._get_param("name")
|
||||||
|
)
|
||||||
|
|
||||||
|
return json.dumps({"pipeline": pipeline, "metadata": metadata})
|
||||||
|
|
||||||
|
def update_pipeline(self):
|
||||||
|
pipeline = self.codepipeline_backend.update_pipeline(
|
||||||
|
self._get_param("pipeline")
|
||||||
|
)
|
||||||
|
|
||||||
|
return json.dumps({"pipeline": pipeline})
|
||||||
|
|
||||||
|
def list_pipelines(self):
|
||||||
|
pipelines = self.codepipeline_backend.list_pipelines()
|
||||||
|
|
||||||
|
return json.dumps({"pipelines": pipelines})
|
||||||
|
|
||||||
|
def delete_pipeline(self):
|
||||||
|
self.codepipeline_backend.delete_pipeline(self._get_param("name"))
|
||||||
|
|
||||||
|
return ""
|
6
moto/codepipeline/urls.py
Normal file
6
moto/codepipeline/urls.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from .responses import CodePipelineResponse
|
||||||
|
|
||||||
|
url_bases = ["https?://codepipeline.(.+).amazonaws.com"]
|
||||||
|
|
||||||
|
url_paths = {"{0}/$": CodePipelineResponse.dispatch}
|
877
tests/test_codepipeline/test_codepipeline.py
Normal file
877
tests/test_codepipeline/test_codepipeline.py
Normal file
@ -0,0 +1,877 @@
|
|||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
import sure # noqa
|
||||||
|
from botocore.exceptions import ClientError
|
||||||
|
from nose.tools import assert_raises
|
||||||
|
|
||||||
|
from moto import mock_codepipeline, mock_iam
|
||||||
|
|
||||||
|
|
||||||
|
@mock_codepipeline
|
||||||
|
def test_create_pipeline():
|
||||||
|
client = boto3.client("codepipeline", region_name="us-east-1")
|
||||||
|
|
||||||
|
response = client.create_pipeline(
|
||||||
|
pipeline={
|
||||||
|
"name": "test-pipeline",
|
||||||
|
"roleArn": get_role_arn(),
|
||||||
|
"artifactStore": {
|
||||||
|
"type": "S3",
|
||||||
|
"location": "codepipeline-us-east-1-123456789012",
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"name": "Stage-1",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Source",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "S3",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"S3Bucket": "test-bucket",
|
||||||
|
"S3ObjectKey": "test-object",
|
||||||
|
},
|
||||||
|
"outputArtifacts": [{"name": "artifact"},],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Stage-2",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Approval",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "Manual",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
tags=[{"key": "key", "value": "value"}],
|
||||||
|
)
|
||||||
|
|
||||||
|
response["pipeline"].should.equal(
|
||||||
|
{
|
||||||
|
"name": "test-pipeline",
|
||||||
|
"roleArn": "arn:aws:iam::123456789012:role/test-role",
|
||||||
|
"artifactStore": {
|
||||||
|
"type": "S3",
|
||||||
|
"location": "codepipeline-us-east-1-123456789012",
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"name": "Stage-1",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Source",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "S3",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"runOrder": 1,
|
||||||
|
"configuration": {
|
||||||
|
"S3Bucket": "test-bucket",
|
||||||
|
"S3ObjectKey": "test-object",
|
||||||
|
},
|
||||||
|
"outputArtifacts": [{"name": "artifact"}],
|
||||||
|
"inputArtifacts": [],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Stage-2",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Approval",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "Manual",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"runOrder": 1,
|
||||||
|
"configuration": {},
|
||||||
|
"outputArtifacts": [],
|
||||||
|
"inputArtifacts": [],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"version": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response["tags"].should.equal([{"key": "key", "value": "value"}])
|
||||||
|
|
||||||
|
|
||||||
|
@mock_codepipeline
|
||||||
|
@mock_iam
|
||||||
|
def test_create_pipeline_errors():
|
||||||
|
client = boto3.client("codepipeline", region_name="us-east-1")
|
||||||
|
client_iam = boto3.client("iam", region_name="us-east-1")
|
||||||
|
client.create_pipeline(
|
||||||
|
pipeline={
|
||||||
|
"name": "test-pipeline",
|
||||||
|
"roleArn": get_role_arn(),
|
||||||
|
"artifactStore": {
|
||||||
|
"type": "S3",
|
||||||
|
"location": "codepipeline-us-east-1-123456789012",
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"name": "Stage-1",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Source",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "S3",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"S3Bucket": "test-bucket",
|
||||||
|
"S3ObjectKey": "test-object",
|
||||||
|
},
|
||||||
|
"outputArtifacts": [{"name": "artifact"},],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Stage-2",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Approval",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "Manual",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
with assert_raises(ClientError) as e:
|
||||||
|
client.create_pipeline(
|
||||||
|
pipeline={
|
||||||
|
"name": "test-pipeline",
|
||||||
|
"roleArn": get_role_arn(),
|
||||||
|
"artifactStore": {
|
||||||
|
"type": "S3",
|
||||||
|
"location": "codepipeline-us-east-1-123456789012",
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"name": "Stage-1",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Source",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "S3",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"S3Bucket": "test-bucket",
|
||||||
|
"S3ObjectKey": "test-object",
|
||||||
|
},
|
||||||
|
"outputArtifacts": [{"name": "artifact"},],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Stage-2",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Approval",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "Manual",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ex = e.exception
|
||||||
|
ex.operation_name.should.equal("CreatePipeline")
|
||||||
|
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||||
|
ex.response["Error"]["Code"].should.contain("InvalidStructureException")
|
||||||
|
ex.response["Error"]["Message"].should.equal(
|
||||||
|
"A pipeline with the name 'test-pipeline' already exists in account '123456789012'"
|
||||||
|
)
|
||||||
|
|
||||||
|
with assert_raises(ClientError) as e:
|
||||||
|
client.create_pipeline(
|
||||||
|
pipeline={
|
||||||
|
"name": "invalid-pipeline",
|
||||||
|
"roleArn": "arn:aws:iam::123456789012:role/not-existing",
|
||||||
|
"artifactStore": {
|
||||||
|
"type": "S3",
|
||||||
|
"location": "codepipeline-us-east-1-123456789012",
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"name": "Stage-1",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Source",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "S3",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"runOrder": 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ex = e.exception
|
||||||
|
ex.operation_name.should.equal("CreatePipeline")
|
||||||
|
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||||
|
ex.response["Error"]["Code"].should.contain("InvalidStructureException")
|
||||||
|
ex.response["Error"]["Message"].should.equal(
|
||||||
|
"CodePipeline is not authorized to perform AssumeRole on role arn:aws:iam::123456789012:role/not-existing"
|
||||||
|
)
|
||||||
|
|
||||||
|
wrong_role_arn = client_iam.create_role(
|
||||||
|
RoleName="wrong-role",
|
||||||
|
AssumeRolePolicyDocument=json.dumps(
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Principal": {"Service": "s3.amazonaws.com"},
|
||||||
|
"Action": "sts:AssumeRole",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)["Role"]["Arn"]
|
||||||
|
|
||||||
|
with assert_raises(ClientError) as e:
|
||||||
|
client.create_pipeline(
|
||||||
|
pipeline={
|
||||||
|
"name": "invalid-pipeline",
|
||||||
|
"roleArn": wrong_role_arn,
|
||||||
|
"artifactStore": {
|
||||||
|
"type": "S3",
|
||||||
|
"location": "codepipeline-us-east-1-123456789012",
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"name": "Stage-1",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Source",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "S3",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"runOrder": 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ex = e.exception
|
||||||
|
ex.operation_name.should.equal("CreatePipeline")
|
||||||
|
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||||
|
ex.response["Error"]["Code"].should.contain("InvalidStructureException")
|
||||||
|
ex.response["Error"]["Message"].should.equal(
|
||||||
|
"CodePipeline is not authorized to perform AssumeRole on role arn:aws:iam::123456789012:role/wrong-role"
|
||||||
|
)
|
||||||
|
|
||||||
|
with assert_raises(ClientError) as e:
|
||||||
|
client.create_pipeline(
|
||||||
|
pipeline={
|
||||||
|
"name": "invalid-pipeline",
|
||||||
|
"roleArn": get_role_arn(),
|
||||||
|
"artifactStore": {
|
||||||
|
"type": "S3",
|
||||||
|
"location": "codepipeline-us-east-1-123456789012",
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"name": "Stage-1",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Source",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "S3",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"runOrder": 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ex = e.exception
|
||||||
|
ex.operation_name.should.equal("CreatePipeline")
|
||||||
|
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||||
|
ex.response["Error"]["Code"].should.contain("InvalidStructureException")
|
||||||
|
ex.response["Error"]["Message"].should.equal(
|
||||||
|
"Pipeline has only 1 stage(s). There should be a minimum of 2 stages in a pipeline"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_codepipeline
|
||||||
|
def test_get_pipeline():
|
||||||
|
client = boto3.client("codepipeline", region_name="us-east-1")
|
||||||
|
client.create_pipeline(
|
||||||
|
pipeline={
|
||||||
|
"name": "test-pipeline",
|
||||||
|
"roleArn": get_role_arn(),
|
||||||
|
"artifactStore": {
|
||||||
|
"type": "S3",
|
||||||
|
"location": "codepipeline-us-east-1-123456789012",
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"name": "Stage-1",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Source",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "S3",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"S3Bucket": "test-bucket",
|
||||||
|
"S3ObjectKey": "test-object",
|
||||||
|
},
|
||||||
|
"outputArtifacts": [{"name": "artifact"},],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Stage-2",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Approval",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "Manual",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
tags=[{"key": "key", "value": "value"}],
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get_pipeline(name="test-pipeline")
|
||||||
|
|
||||||
|
response["pipeline"].should.equal(
|
||||||
|
{
|
||||||
|
"name": "test-pipeline",
|
||||||
|
"roleArn": "arn:aws:iam::123456789012:role/test-role",
|
||||||
|
"artifactStore": {
|
||||||
|
"type": "S3",
|
||||||
|
"location": "codepipeline-us-east-1-123456789012",
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"name": "Stage-1",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Source",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "S3",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"runOrder": 1,
|
||||||
|
"configuration": {
|
||||||
|
"S3Bucket": "test-bucket",
|
||||||
|
"S3ObjectKey": "test-object",
|
||||||
|
},
|
||||||
|
"outputArtifacts": [{"name": "artifact"}],
|
||||||
|
"inputArtifacts": [],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Stage-2",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Approval",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "Manual",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"runOrder": 1,
|
||||||
|
"configuration": {},
|
||||||
|
"outputArtifacts": [],
|
||||||
|
"inputArtifacts": [],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"version": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response["metadata"]["pipelineArn"].should.equal(
|
||||||
|
"arn:aws:codepipeline:us-east-1:123456789012:test-pipeline"
|
||||||
|
)
|
||||||
|
response["metadata"]["created"].should.be.a(datetime)
|
||||||
|
response["metadata"]["updated"].should.be.a(datetime)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_codepipeline
|
||||||
|
def test_get_pipeline_errors():
|
||||||
|
client = boto3.client("codepipeline", region_name="us-east-1")
|
||||||
|
|
||||||
|
with assert_raises(ClientError) as e:
|
||||||
|
client.get_pipeline(name="not-existing")
|
||||||
|
ex = e.exception
|
||||||
|
ex.operation_name.should.equal("GetPipeline")
|
||||||
|
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||||
|
ex.response["Error"]["Code"].should.contain("PipelineNotFoundException")
|
||||||
|
ex.response["Error"]["Message"].should.equal(
|
||||||
|
"Account '123456789012' does not have a pipeline with name 'not-existing'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_codepipeline
|
||||||
|
def test_update_pipeline():
|
||||||
|
client = boto3.client("codepipeline", region_name="us-east-1")
|
||||||
|
role_arn = get_role_arn()
|
||||||
|
client.create_pipeline(
|
||||||
|
pipeline={
|
||||||
|
"name": "test-pipeline",
|
||||||
|
"roleArn": role_arn,
|
||||||
|
"artifactStore": {
|
||||||
|
"type": "S3",
|
||||||
|
"location": "codepipeline-us-east-1-123456789012",
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"name": "Stage-1",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Source",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "S3",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"S3Bucket": "test-bucket",
|
||||||
|
"S3ObjectKey": "test-object",
|
||||||
|
},
|
||||||
|
"outputArtifacts": [{"name": "artifact"},],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Stage-2",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Approval",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "Manual",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
tags=[{"key": "key", "value": "value"}],
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get_pipeline(name="test-pipeline")
|
||||||
|
created_time = response["metadata"]["created"]
|
||||||
|
updated_time = response["metadata"]["updated"]
|
||||||
|
|
||||||
|
response = client.update_pipeline(
|
||||||
|
pipeline={
|
||||||
|
"name": "test-pipeline",
|
||||||
|
"roleArn": role_arn,
|
||||||
|
"artifactStore": {
|
||||||
|
"type": "S3",
|
||||||
|
"location": "codepipeline-us-east-1-123456789012",
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"name": "Stage-1",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Source",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "S3",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"S3Bucket": "different-bucket",
|
||||||
|
"S3ObjectKey": "test-object",
|
||||||
|
},
|
||||||
|
"outputArtifacts": [{"name": "artifact"},],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Stage-2",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Approval",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "Manual",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
response["pipeline"].should.equal(
|
||||||
|
{
|
||||||
|
"name": "test-pipeline",
|
||||||
|
"roleArn": "arn:aws:iam::123456789012:role/test-role",
|
||||||
|
"artifactStore": {
|
||||||
|
"type": "S3",
|
||||||
|
"location": "codepipeline-us-east-1-123456789012",
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"name": "Stage-1",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Source",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "S3",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"runOrder": 1,
|
||||||
|
"configuration": {
|
||||||
|
"S3Bucket": "different-bucket",
|
||||||
|
"S3ObjectKey": "test-object",
|
||||||
|
},
|
||||||
|
"outputArtifacts": [{"name": "artifact"}],
|
||||||
|
"inputArtifacts": [],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Stage-2",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Approval",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "Manual",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"runOrder": 1,
|
||||||
|
"configuration": {},
|
||||||
|
"outputArtifacts": [],
|
||||||
|
"inputArtifacts": [],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"version": 2,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
metadata = client.get_pipeline(name="test-pipeline")["metadata"]
|
||||||
|
metadata["created"].should.equal(created_time)
|
||||||
|
metadata["updated"].should.be.greater_than(updated_time)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_codepipeline
|
||||||
|
def test_update_pipeline_errors():
|
||||||
|
client = boto3.client("codepipeline", region_name="us-east-1")
|
||||||
|
|
||||||
|
with assert_raises(ClientError) as e:
|
||||||
|
client.update_pipeline(
|
||||||
|
pipeline={
|
||||||
|
"name": "not-existing",
|
||||||
|
"roleArn": get_role_arn(),
|
||||||
|
"artifactStore": {
|
||||||
|
"type": "S3",
|
||||||
|
"location": "codepipeline-us-east-1-123456789012",
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"name": "Stage-1",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Source",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "S3",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"S3Bucket": "test-bucket",
|
||||||
|
"S3ObjectKey": "test-object",
|
||||||
|
},
|
||||||
|
"outputArtifacts": [{"name": "artifact"},],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Stage-2",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Approval",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "Manual",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ex = e.exception
|
||||||
|
ex.operation_name.should.equal("UpdatePipeline")
|
||||||
|
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
||||||
|
ex.response["Error"]["Code"].should.contain("ResourceNotFoundException")
|
||||||
|
ex.response["Error"]["Message"].should.equal(
|
||||||
|
"The account with id '123456789012' does not include a pipeline with the name 'not-existing'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_codepipeline
|
||||||
|
def test_list_pipelines():
|
||||||
|
client = boto3.client("codepipeline", region_name="us-east-1")
|
||||||
|
client.create_pipeline(
|
||||||
|
pipeline={
|
||||||
|
"name": "test-pipeline-1",
|
||||||
|
"roleArn": get_role_arn(),
|
||||||
|
"artifactStore": {
|
||||||
|
"type": "S3",
|
||||||
|
"location": "codepipeline-us-east-1-123456789012",
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"name": "Stage-1",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Source",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "S3",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"S3Bucket": "test-bucket",
|
||||||
|
"S3ObjectKey": "test-object",
|
||||||
|
},
|
||||||
|
"outputArtifacts": [{"name": "artifact"},],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Stage-2",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Approval",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "Manual",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
client.create_pipeline(
|
||||||
|
pipeline={
|
||||||
|
"name": "test-pipeline-2",
|
||||||
|
"roleArn": get_role_arn(),
|
||||||
|
"artifactStore": {
|
||||||
|
"type": "S3",
|
||||||
|
"location": "codepipeline-us-east-1-123456789012",
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"name": "Stage-1",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Source",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "S3",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"S3Bucket": "test-bucket",
|
||||||
|
"S3ObjectKey": "test-object",
|
||||||
|
},
|
||||||
|
"outputArtifacts": [{"name": "artifact"},],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Stage-2",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Approval",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "Manual",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.list_pipelines()
|
||||||
|
|
||||||
|
response["pipelines"].should.have.length_of(2)
|
||||||
|
response["pipelines"][0]["name"].should.equal("test-pipeline-1")
|
||||||
|
response["pipelines"][0]["version"].should.equal(1)
|
||||||
|
response["pipelines"][0]["created"].should.be.a(datetime)
|
||||||
|
response["pipelines"][0]["updated"].should.be.a(datetime)
|
||||||
|
response["pipelines"][1]["name"].should.equal("test-pipeline-2")
|
||||||
|
response["pipelines"][1]["version"].should.equal(1)
|
||||||
|
response["pipelines"][1]["created"].should.be.a(datetime)
|
||||||
|
response["pipelines"][1]["updated"].should.be.a(datetime)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_codepipeline
|
||||||
|
def test_delete_pipeline():
|
||||||
|
client = boto3.client("codepipeline", region_name="us-east-1")
|
||||||
|
client.create_pipeline(
|
||||||
|
pipeline={
|
||||||
|
"name": "test-pipeline",
|
||||||
|
"roleArn": get_role_arn(),
|
||||||
|
"artifactStore": {
|
||||||
|
"type": "S3",
|
||||||
|
"location": "codepipeline-us-east-1-123456789012",
|
||||||
|
},
|
||||||
|
"stages": [
|
||||||
|
{
|
||||||
|
"name": "Stage-1",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Source",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "S3",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"S3Bucket": "test-bucket",
|
||||||
|
"S3ObjectKey": "test-object",
|
||||||
|
},
|
||||||
|
"outputArtifacts": [{"name": "artifact"},],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Stage-2",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "Action-1",
|
||||||
|
"actionTypeId": {
|
||||||
|
"category": "Approval",
|
||||||
|
"owner": "AWS",
|
||||||
|
"provider": "Manual",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
client.list_pipelines()["pipelines"].should.have.length_of(1)
|
||||||
|
|
||||||
|
client.delete_pipeline(name="test-pipeline")
|
||||||
|
|
||||||
|
client.list_pipelines()["pipelines"].should.have.length_of(0)
|
||||||
|
|
||||||
|
# deleting a not existing pipeline, should raise no exception
|
||||||
|
client.delete_pipeline(name="test-pipeline")
|
||||||
|
|
||||||
|
|
||||||
|
@mock_iam
|
||||||
|
def get_role_arn():
|
||||||
|
iam = boto3.client("iam", region_name="us-east-1")
|
||||||
|
try:
|
||||||
|
return iam.get_role(RoleName="test-role")["Role"]["Arn"]
|
||||||
|
except ClientError:
|
||||||
|
return iam.create_role(
|
||||||
|
RoleName="test-role",
|
||||||
|
AssumeRolePolicyDocument=json.dumps(
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Principal": {"Service": "codepipeline.amazonaws.com"},
|
||||||
|
"Action": "sts:AssumeRole",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)["Role"]["Arn"]
|
Loading…
x
Reference in New Issue
Block a user