Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add resources command #34

Merged
merged 1 commit into from Feb 21, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Expand Up @@ -117,6 +117,17 @@ root@62d81801cc09:/app/examples/s3-bucket# aws s3 ls
2017-02-15 11:21:18 formica-example-stack-deploymentbucket-57ouvt2o46yh
```

We can also check out all the resources for a specific stack with the resources command:

```
root@67c57a89511a:/app/docs/examples/s3-bucket# formica resources --stack formica-example-stack
+------------------+------------------------------------------------------+-----------------+-----------------+
| Logical ID | Physical ID | Type | Status |
+==================+======================================================+=================+=================+
| DeploymentBucket | formica-example-stack-deploymentbucket-57ouvt2o46yh | AWS::S3::Bucket | CREATE_COMPLETE |
+------------------+------------------------------------------------------+-----------------+-----------------+
```

If we want to add an additional bucket we can change our `bucket.fc` file to be the following:

```shell
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Expand Up @@ -6,6 +6,7 @@
* [describe:](commands/describe.md) Describe the latest change set
* [new:](commands/new.md) Create a change set for a new stack
* [remove:](commands/remove.md) Remove the configured stack
* [resources:](commands/resources.md) List all resources of a stack
* [stacks:](commands/stacks.md) List all stacks
* [template:](commands/template.md) Print the current template
2. [Template files reference](template-files.md)
Expand Down
22 changes: 22 additions & 0 deletions docs/commands/resources.md
@@ -0,0 +1,22 @@
# `formica resources`

Through the resources command you can list all resources for a deployed stack so its easy for you to get the physical id of a resource you deployed.

The command will print the logical id, physical id, type and status.

## Example

```shell
root@67c57a89511a:/app/docs/examples/s3-bucket# formica resources --stack formica-example-stack
+------------------+------------------------------------------------------+-----------------+-----------------+
| Logical ID | Physical ID | Type | Status |
+==================+======================================================+=================+=================+
| DeploymentBucket | formica-example-stack-deploymentbucket-1tzvltuaftxso | AWS::S3::Bucket | CREATE_COMPLETE |
+------------------+------------------------------------------------------+-----------------+-----------------+
```

## Options

* `--stack STACK` The stack you want to remove. [required]
* `--profile PROFILE` The AWS profile to use.
* `--region REGION` The AWS region to use.
25 changes: 25 additions & 0 deletions formica/cli.py
Expand Up @@ -21,6 +21,7 @@
from .loader import Loader

STACK_HEADERS = ['Name', 'Created At', 'Updated At', 'Status']
RESOURCE_HEADERS = ['Logical ID', 'Physical ID', 'Type', 'Status']


def aws_options(f):
Expand Down Expand Up @@ -178,3 +179,27 @@ def remove(stack):
last_event = client.describe_stack_events(StackName=stack)['StackEvents'][0]['EventId']
client.delete_stack(StackName=stack)
StackWaiter(stack_id, client).wait(last_event)


@main.command()
@stack('The stack see the resources for.')
@aws_exceptions
@aws_options
def resources(stack):
"""List all resources of a stack"""
client = AWS.current_session().client('cloudformation')
paginator = client.get_paginator('list_stack_resources').paginate(StackName=stack)

table = Texttable(max_width=150)
table.add_rows([RESOURCE_HEADERS])

for page in paginator:
for resource in page['StackResourceSummaries']:
table.add_row(
[resource['LogicalResourceId'],
resource['PhysicalResourceId'],
resource['ResourceType'],
resource['ResourceStatus']
])

click.echo(table.draw() + "\n")
4 changes: 4 additions & 0 deletions tests/integration/test_basic.py
Expand Up @@ -56,6 +56,10 @@ def run_formica(*args):
deploy = run_formica('deploy', *stack_args)
assert 'UPDATE_COMPLETE' in deploy

# Deploy changes to existing stack
resources = run_formica('resources', *stack_args)
assert 'TestNameUpdate' in resources

# List all existing stacks
stacks = run_formica('stacks')
assert stack_name in stacks
Expand Down
8 changes: 5 additions & 3 deletions tests/unit/constants.py
@@ -1,8 +1,9 @@

from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from future import standard_library

standard_library.install_aliases()
import datetime

Expand All @@ -24,3 +25,4 @@
MESSAGE = 'TESTMESSAGE'
EVENT_ID = 'SomeEventID'
STACK_EVENTS = {'StackEvents': [{'StackId': 'arn:aws:cloudformation:eu-central-1:420759548424:stack/teststack/a29eaa70-e7ab-11e6-aada-503f2ad2e536', 'EventId': '7e7e22e0-ec85-11e6-a72f-50a68a770ce6', 'StackName': 'teststack', 'LogicalResourceId': 'teststack', 'PhysicalResourceId': 'arn:aws:cloudformation:eu-central-1:420759548424:stack/teststack/a29eaa70-e7ab-11e6-aada-503f2ad2e536', 'ResourceType': 'AWS::CloudFormation::Stack', 'Timestamp': datetime.datetime(2017, 2, 6, 16, 1, 17, 611000, tzinfo=tzlocal()), 'ResourceStatus': 'UPDATE_COMPLETE', "ResourceStatusReason": "Resource creation Initiated"}, {'StackId': 'arn:aws:cloudformation:eu-central-1:420759548424:stack/teststack/a29eaa70-e7ab-11e6-aada-503f2ad2e536', 'EventId': 'DeploymentBucket14-bc35d70a-3df5-45d4-afaa-c06b536a50cd', 'StackName': 'teststack', 'LogicalResourceId': 'DeploymentBucket14', 'PhysicalResourceId': 'teststack-deploymentbucket14-1r1yxsi27kclv', 'ResourceType': 'AWS::S3::Bucket', 'Timestamp': datetime.datetime(2017, 2, 6, 16, 1, 16, 976000), 'ResourceStatus': 'DELETE_COMPLETE'}, {'StackId': 'arn:aws:cloudformation:eu-central-1:420759548424:stack/teststack/a29eaa70-e7ab-11e6-aada-503f2ad2e536', 'EventId': 'DeploymentBucket18-f9a5ef79-307a-4919-a293-b74be40e19b2', 'StackName': 'teststack', 'LogicalResourceId': 'DeploymentBucket18', 'PhysicalResourceId': 'teststack-deploymentbucket18-iy7lt61peqvp', 'ResourceType': 'AWS::S3::Bucket', 'Timestamp': datetime.datetime(2017, 2, 6, 16, 1, 16, 868000), 'ResourceStatus': 'DELETE_COMPLETE'}, {'StackId': 'arn:aws:cloudformation:eu-central-1:420759548424:stack/teststack/a29eaa70-e7ab-11e6-aada-503f2ad2e536', 'EventId': 'DeploymentBucket3-7c92066b-c2e7-427a-ab29-53b928925473', 'StackName': 'teststack', 'LogicalResourceId': 'DeploymentBucket3', 'PhysicalResourceId': 'teststack-deploymentbucket3-51e2v1veq7go', 'ResourceType': 'AWS::S3::Bucket', 'Timestamp': datetime.datetime(2017, 2, 6, 16, 1, 16, 852000), 'ResourceStatus': 'DELETE_COMPLETE'}, {'StackId': 'arn:aws:cloudformation:eu-central-1:420759548424:stack/teststack/a29eaa70-e7ab-11e6-aada-503f2ad2e536', 'EventId': 'DeploymentBucket15-4ce4845e-f072-4d69-9aae-d2e8105dc0a8', 'StackName': 'teststack', 'LogicalResourceId': 'DeploymentBucket15', 'PhysicalResourceId': 'teststack-deploymentbucket15-2tdubysims21', 'ResourceType': 'AWS::S3::Bucket', 'Timestamp': datetime.datetime(2017, 2, 6, 16, 1, 16, 712000), 'ResourceStatus': 'DELETE_COMPLETE'}]} # noqa
LIST_STACK_RESOURCES = {'ResponseMetadata': {'RetryAttempts': 0, 'HTTPStatusCode': 200, 'RequestId': '516e9109-f819-11e6-816b-69534f0a2126', 'HTTPHeaders': {'x-amzn-requestid': '516e9109-f819-11e6-816b-69534f0a2126', 'vary': 'Accept-Encoding', 'content-length': '2121', 'content-type': 'text/xml', 'date': 'Tue, 21 Feb 2017 09:37:10 GMT'}}, u'StackResourceSummaries': [{u'ResourceType': 'AWS::Route53::HostedZone', u'PhysicalResourceId': 'ZAYGDOKFPYFK6', u'LastUpdatedTimestamp': datetime.datetime(2017, 1, 13, 16, 13, 25, 978000, tzinfo=tzlocal()), u'ResourceStatus': 'CREATE_COMPLETE', u'LogicalResourceId': 'FlomotlikMe'}]} # noqa
4 changes: 2 additions & 2 deletions tests/unit/test_cli.py
Expand Up @@ -10,11 +10,11 @@
from botocore.exceptions import ProfileNotFound, NoCredentialsError, NoRegionError, ClientError
from click.testing import CliRunner

from formica.cli import change, deploy, new, stacks, remove
from formica.cli import change, deploy, new, stacks, remove, resources
from formica.helper import aws_exceptions
from tests.unit.constants import STACK, MESSAGE

METHODS = [change, deploy, new, remove]
METHODS = [change, deploy, new, remove, resources]
NO_STACK_METHODS = [stacks]


Expand Down
50 changes: 50 additions & 0 deletions tests/unit/test_resources.py
@@ -0,0 +1,50 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from future import standard_library

standard_library.install_aliases()
import unittest
import mock
from mock import patch, Mock

from click.testing import CliRunner

from formica import cli
from formica.cli import RESOURCE_HEADERS
from tests.unit.constants import STACK, LIST_STACK_RESOURCES


class TestResources(unittest.TestCase):
def run_resources(self, exit_code=0):
runner = CliRunner()
result = runner.invoke(cli.resources, ['--stack', STACK])
print(result.output)
self.assertEqual(result.exit_code, exit_code)
return result

@patch('formica.cli.click')
@patch('formica.aws.Session')
def test_print_stacks(self, session, click):
client_mock = Mock()
session.return_value.client.return_value = client_mock
client_mock.get_paginator.return_value.paginate.return_value = [LIST_STACK_RESOURCES]
self.run_resources()

client_mock.get_paginator.assert_called_with('list_stack_resources')
client_mock.get_paginator.return_value.paginate.assert_called_with(StackName=STACK)

click.echo.assert_called_with(mock.ANY)
args = click.echo.call_args[0]

to_search = []
to_search.extend(RESOURCE_HEADERS)
to_search.extend(['AWS::Route53::HostedZone'])
to_search.extend(['FlomotlikMe'])
to_search.extend(['CREATE_COMPLETE'])
to_search.extend(['ZAYGDOKFPYFK6'])
change_set_output = args[0]
for term in to_search:
self.assertIn(term, change_set_output)
self.assertNotIn('None', change_set_output)