Merge pull request #2652 from gruebel/add-codepipeline-tags

Add codepipeline tags
This commit is contained in:
Mike Grima 2019-12-27 08:57:59 -08:00 committed by GitHub
commit b86e1d073b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 318 additions and 377 deletions

View File

@ -1374,7 +1374,7 @@
- [ ] update_profiling_group - [ ] update_profiling_group
## codepipeline ## codepipeline
13% implemented 22% implemented
- [ ] acknowledge_job - [ ] acknowledge_job
- [ ] acknowledge_third_party_job - [ ] acknowledge_third_party_job
- [ ] create_custom_action_type - [ ] create_custom_action_type
@ -1394,7 +1394,7 @@
- [ ] list_action_types - [ ] list_action_types
- [ ] list_pipeline_executions - [ ] list_pipeline_executions
- [X] list_pipelines - [X] list_pipelines
- [ ] list_tags_for_resource - [X] list_tags_for_resource
- [ ] list_webhooks - [ ] list_webhooks
- [ ] poll_for_jobs - [ ] poll_for_jobs
- [ ] poll_for_third_party_jobs - [ ] poll_for_third_party_jobs
@ -1408,8 +1408,8 @@
- [ ] register_webhook_with_third_party - [ ] register_webhook_with_third_party
- [ ] retry_stage_execution - [ ] retry_stage_execution
- [ ] start_pipeline_execution - [ ] start_pipeline_execution
- [ ] tag_resource - [X] tag_resource
- [ ] untag_resource - [X] untag_resource
- [X] update_pipeline - [X] update_pipeline
## codestar ## codestar

View File

@ -26,3 +26,19 @@ class ResourceNotFoundException(JsonRESTError):
super(ResourceNotFoundException, self).__init__( super(ResourceNotFoundException, self).__init__(
"ResourceNotFoundException", message "ResourceNotFoundException", message
) )
class InvalidTagsException(JsonRESTError):
code = 400
def __init__(self, message):
super(InvalidTagsException, self).__init__("InvalidTagsException", message)
class TooManyTagsException(JsonRESTError):
code = 400
def __init__(self, arn):
super(TooManyTagsException, self).__init__(
"TooManyTagsException", "Tag limit exceeded for resource [{}].".format(arn)
)

View File

@ -12,6 +12,8 @@ from moto.codepipeline.exceptions import (
InvalidStructureException, InvalidStructureException,
PipelineNotFoundException, PipelineNotFoundException,
ResourceNotFoundException, ResourceNotFoundException,
InvalidTagsException,
TooManyTagsException,
) )
from moto.core import BaseBackend, BaseModel from moto.core import BaseBackend, BaseModel
@ -54,6 +56,18 @@ class CodePipeline(BaseModel):
return pipeline return pipeline
def validate_tags(self, tags):
for tag in tags:
if tag["key"].startswith("aws:"):
raise InvalidTagsException(
"Not allowed to modify system tags. "
"System tags start with 'aws:'. "
"msg=[Caller is an end user and not allowed to mutate system tags]"
)
if (len(self.tags) + len(tags)) > 50:
raise TooManyTagsException(self._arn)
class CodePipelineBackend(BaseBackend): class CodePipelineBackend(BaseBackend):
def __init__(self): def __init__(self):
@ -93,10 +107,12 @@ class CodePipelineBackend(BaseBackend):
self.pipelines[pipeline["name"]] = CodePipeline(region, pipeline) self.pipelines[pipeline["name"]] = CodePipeline(region, pipeline)
if tags: if tags:
self.pipelines[pipeline["name"]].validate_tags(tags)
new_tags = {tag["key"]: tag["value"] for tag in tags} new_tags = {tag["key"]: tag["value"] for tag in tags}
self.pipelines[pipeline["name"]].tags.update(new_tags) self.pipelines[pipeline["name"]].tags.update(new_tags)
return pipeline, tags return pipeline, sorted(tags, key=lambda i: i["key"])
def get_pipeline(self, name): def get_pipeline(self, name):
codepipeline = self.pipelines.get(name) codepipeline = self.pipelines.get(name)
@ -145,6 +161,51 @@ class CodePipelineBackend(BaseBackend):
def delete_pipeline(self, name): def delete_pipeline(self, name):
self.pipelines.pop(name, None) self.pipelines.pop(name, None)
def list_tags_for_resource(self, arn):
name = arn.split(":")[-1]
pipeline = self.pipelines.get(name)
if not pipeline:
raise ResourceNotFoundException(
"The account with id '{0}' does not include a pipeline with the name '{1}'".format(
ACCOUNT_ID, name
)
)
tags = [{"key": key, "value": value} for key, value in pipeline.tags.items()]
return sorted(tags, key=lambda i: i["key"])
def tag_resource(self, arn, tags):
name = arn.split(":")[-1]
pipeline = self.pipelines.get(name)
if not pipeline:
raise ResourceNotFoundException(
"The account with id '{0}' does not include a pipeline with the name '{1}'".format(
ACCOUNT_ID, name
)
)
pipeline.validate_tags(tags)
for tag in tags:
pipeline.tags.update({tag["key"]: tag["value"]})
def untag_resource(self, arn, tag_keys):
name = arn.split(":")[-1]
pipeline = self.pipelines.get(name)
if not pipeline:
raise ResourceNotFoundException(
"The account with id '{0}' does not include a pipeline with the name '{1}'".format(
ACCOUNT_ID, name
)
)
for key in tag_keys:
pipeline.tags.pop(key, None)
codepipeline_backends = {} codepipeline_backends = {}
for region in Session().get_available_regions("codepipeline"): for region in Session().get_available_regions("codepipeline"):

View File

@ -39,3 +39,24 @@ class CodePipelineResponse(BaseResponse):
self.codepipeline_backend.delete_pipeline(self._get_param("name")) self.codepipeline_backend.delete_pipeline(self._get_param("name"))
return "" return ""
def list_tags_for_resource(self):
tags = self.codepipeline_backend.list_tags_for_resource(
self._get_param("resourceArn")
)
return json.dumps({"tags": tags})
def tag_resource(self):
self.codepipeline_backend.tag_resource(
self._get_param("resourceArn"), self._get_param("tags")
)
return ""
def untag_resource(self):
self.codepipeline_backend.untag_resource(
self._get_param("resourceArn"), self._get_param("tagKeys")
)
return ""

View File

@ -13,52 +13,7 @@ from moto import mock_codepipeline, mock_iam
def test_create_pipeline(): def test_create_pipeline():
client = boto3.client("codepipeline", region_name="us-east-1") client = boto3.client("codepipeline", region_name="us-east-1")
response = client.create_pipeline( response = create_basic_codepipeline(client, "test-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( response["pipeline"].should.equal(
{ {
@ -120,98 +75,10 @@ def test_create_pipeline():
def test_create_pipeline_errors(): def test_create_pipeline_errors():
client = boto3.client("codepipeline", region_name="us-east-1") client = boto3.client("codepipeline", region_name="us-east-1")
client_iam = boto3.client("iam", region_name="us-east-1") client_iam = boto3.client("iam", region_name="us-east-1")
client.create_pipeline( create_basic_codepipeline(client, "test-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: with assert_raises(ClientError) as e:
client.create_pipeline( create_basic_codepipeline(client, "test-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 = e.exception
ex.operation_name.should.equal("CreatePipeline") ex.operation_name.should.equal("CreatePipeline")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
@ -348,52 +215,7 @@ def test_create_pipeline_errors():
@mock_codepipeline @mock_codepipeline
def test_get_pipeline(): def test_get_pipeline():
client = boto3.client("codepipeline", region_name="us-east-1") client = boto3.client("codepipeline", region_name="us-east-1")
client.create_pipeline( create_basic_codepipeline(client, "test-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 = client.get_pipeline(name="test-pipeline")
@ -474,53 +296,7 @@ def test_get_pipeline_errors():
@mock_codepipeline @mock_codepipeline
def test_update_pipeline(): def test_update_pipeline():
client = boto3.client("codepipeline", region_name="us-east-1") client = boto3.client("codepipeline", region_name="us-east-1")
role_arn = get_role_arn() create_basic_codepipeline(client, "test-pipeline")
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") response = client.get_pipeline(name="test-pipeline")
created_time = response["metadata"]["created"] created_time = response["metadata"]["created"]
@ -529,7 +305,7 @@ def test_update_pipeline():
response = client.update_pipeline( response = client.update_pipeline(
pipeline={ pipeline={
"name": "test-pipeline", "name": "test-pipeline",
"roleArn": role_arn, "roleArn": get_role_arn(),
"artifactStore": { "artifactStore": {
"type": "S3", "type": "S3",
"location": "codepipeline-us-east-1-123456789012", "location": "codepipeline-us-east-1-123456789012",
@ -692,105 +468,19 @@ def test_update_pipeline_errors():
@mock_codepipeline @mock_codepipeline
def test_list_pipelines(): def test_list_pipelines():
client = boto3.client("codepipeline", region_name="us-east-1") client = boto3.client("codepipeline", region_name="us-east-1")
client.create_pipeline( name_1 = "test-pipeline-1"
pipeline={ create_basic_codepipeline(client, name_1)
"name": "test-pipeline-1", name_2 = "test-pipeline-2"
"roleArn": get_role_arn(), create_basic_codepipeline(client, name_2)
"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 = client.list_pipelines()
response["pipelines"].should.have.length_of(2) response["pipelines"].should.have.length_of(2)
response["pipelines"][0]["name"].should.equal("test-pipeline-1") response["pipelines"][0]["name"].should.equal(name_1)
response["pipelines"][0]["version"].should.equal(1) response["pipelines"][0]["version"].should.equal(1)
response["pipelines"][0]["created"].should.be.a(datetime) response["pipelines"][0]["created"].should.be.a(datetime)
response["pipelines"][0]["updated"].should.be.a(datetime) response["pipelines"][0]["updated"].should.be.a(datetime)
response["pipelines"][1]["name"].should.equal("test-pipeline-2") response["pipelines"][1]["name"].should.equal(name_2)
response["pipelines"][1]["version"].should.equal(1) response["pipelines"][1]["version"].should.equal(1)
response["pipelines"][1]["created"].should.be.a(datetime) response["pipelines"][1]["created"].should.be.a(datetime)
response["pipelines"][1]["updated"].should.be.a(datetime) response["pipelines"][1]["updated"].should.be.a(datetime)
@ -799,68 +489,172 @@ def test_list_pipelines():
@mock_codepipeline @mock_codepipeline
def test_delete_pipeline(): def test_delete_pipeline():
client = boto3.client("codepipeline", region_name="us-east-1") client = boto3.client("codepipeline", region_name="us-east-1")
client.create_pipeline( name = "test-pipeline"
pipeline={ create_basic_codepipeline(client, name)
"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.list_pipelines()["pipelines"].should.have.length_of(1)
client.delete_pipeline(name="test-pipeline") client.delete_pipeline(name=name)
client.list_pipelines()["pipelines"].should.have.length_of(0) client.list_pipelines()["pipelines"].should.have.length_of(0)
# deleting a not existing pipeline, should raise no exception # deleting a not existing pipeline, should raise no exception
client.delete_pipeline(name="test-pipeline") client.delete_pipeline(name=name)
@mock_codepipeline
def test_list_tags_for_resource():
client = boto3.client("codepipeline", region_name="us-east-1")
name = "test-pipeline"
create_basic_codepipeline(client, name)
response = client.list_tags_for_resource(
resourceArn="arn:aws:codepipeline:us-east-1:123456789012:{}".format(name)
)
response["tags"].should.equal([{"key": "key", "value": "value"}])
@mock_codepipeline
def test_list_tags_for_resource_errors():
client = boto3.client("codepipeline", region_name="us-east-1")
with assert_raises(ClientError) as e:
client.list_tags_for_resource(
resourceArn="arn:aws:codepipeline:us-east-1:123456789012:not-existing"
)
ex = e.exception
ex.operation_name.should.equal("ListTagsForResource")
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_tag_resource():
client = boto3.client("codepipeline", region_name="us-east-1")
name = "test-pipeline"
create_basic_codepipeline(client, name)
client.tag_resource(
resourceArn="arn:aws:codepipeline:us-east-1:123456789012:{}".format(name),
tags=[{"key": "key-2", "value": "value-2"}],
)
response = client.list_tags_for_resource(
resourceArn="arn:aws:codepipeline:us-east-1:123456789012:{}".format(name)
)
response["tags"].should.equal(
[{"key": "key", "value": "value"}, {"key": "key-2", "value": "value-2"}]
)
@mock_codepipeline
def test_tag_resource_errors():
client = boto3.client("codepipeline", region_name="us-east-1")
name = "test-pipeline"
create_basic_codepipeline(client, name)
with assert_raises(ClientError) as e:
client.tag_resource(
resourceArn="arn:aws:codepipeline:us-east-1:123456789012:not-existing",
tags=[{"key": "key-2", "value": "value-2"}],
)
ex = e.exception
ex.operation_name.should.equal("TagResource")
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'"
)
with assert_raises(ClientError) as e:
client.tag_resource(
resourceArn="arn:aws:codepipeline:us-east-1:123456789012:{}".format(name),
tags=[{"key": "aws:key", "value": "value"}],
)
ex = e.exception
ex.operation_name.should.equal("TagResource")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.response["Error"]["Code"].should.contain("InvalidTagsException")
ex.response["Error"]["Message"].should.equal(
"Not allowed to modify system tags. "
"System tags start with 'aws:'. "
"msg=[Caller is an end user and not allowed to mutate system tags]"
)
with assert_raises(ClientError) as e:
client.tag_resource(
resourceArn="arn:aws:codepipeline:us-east-1:123456789012:{}".format(name),
tags=[
{"key": "key-{}".format(i), "value": "value-{}".format(i)}
for i in range(50)
],
)
ex = e.exception
ex.operation_name.should.equal("TagResource")
ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
ex.response["Error"]["Code"].should.contain("TooManyTagsException")
ex.response["Error"]["Message"].should.equal(
"Tag limit exceeded for resource [arn:aws:codepipeline:us-east-1:123456789012:{}].".format(
name
)
)
@mock_codepipeline
def test_untag_resource():
client = boto3.client("codepipeline", region_name="us-east-1")
name = "test-pipeline"
create_basic_codepipeline(client, name)
response = client.list_tags_for_resource(
resourceArn="arn:aws:codepipeline:us-east-1:123456789012:{}".format(name)
)
response["tags"].should.equal([{"key": "key", "value": "value"}])
client.untag_resource(
resourceArn="arn:aws:codepipeline:us-east-1:123456789012:{}".format(name),
tagKeys=["key"],
)
response = client.list_tags_for_resource(
resourceArn="arn:aws:codepipeline:us-east-1:123456789012:{}".format(name)
)
response["tags"].should.have.length_of(0)
# removing a not existing tag should raise no exception
client.untag_resource(
resourceArn="arn:aws:codepipeline:us-east-1:123456789012:{}".format(name),
tagKeys=["key"],
)
@mock_codepipeline
def test_untag_resource_errors():
client = boto3.client("codepipeline", region_name="us-east-1")
with assert_raises(ClientError) as e:
client.untag_resource(
resourceArn="arn:aws:codepipeline:us-east-1:123456789012:not-existing",
tagKeys=["key"],
)
ex = e.exception
ex.operation_name.should.equal("UntagResource")
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_iam @mock_iam
def get_role_arn(): def get_role_arn():
iam = boto3.client("iam", region_name="us-east-1") client = boto3.client("iam", region_name="us-east-1")
try: try:
return iam.get_role(RoleName="test-role")["Role"]["Arn"] return client.get_role(RoleName="test-role")["Role"]["Arn"]
except ClientError: except ClientError:
return iam.create_role( return client.create_role(
RoleName="test-role", RoleName="test-role",
AssumeRolePolicyDocument=json.dumps( AssumeRolePolicyDocument=json.dumps(
{ {
@ -875,3 +669,52 @@ def get_role_arn():
} }
), ),
)["Role"]["Arn"] )["Role"]["Arn"]
def create_basic_codepipeline(client, name):
return client.create_pipeline(
pipeline={
"name": name,
"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"}],
)