awslabs.ccapi-mcp-server 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of awslabs.ccapi-mcp-server might be problematic. Click here for more details.
- awslabs/__init__.py +16 -0
- awslabs/ccapi_mcp_server/__init__.py +17 -0
- awslabs/ccapi_mcp_server/aws_client.py +62 -0
- awslabs/ccapi_mcp_server/cloud_control_utils.py +120 -0
- awslabs/ccapi_mcp_server/context.py +37 -0
- awslabs/ccapi_mcp_server/errors.py +67 -0
- awslabs/ccapi_mcp_server/iac_generator.py +203 -0
- awslabs/ccapi_mcp_server/impl/__init__.py +13 -0
- awslabs/ccapi_mcp_server/impl/tools/__init__.py +13 -0
- awslabs/ccapi_mcp_server/impl/tools/explanation.py +325 -0
- awslabs/ccapi_mcp_server/impl/tools/infrastructure_generation.py +70 -0
- awslabs/ccapi_mcp_server/impl/tools/resource_operations.py +367 -0
- awslabs/ccapi_mcp_server/impl/tools/security_scanning.py +223 -0
- awslabs/ccapi_mcp_server/impl/tools/session_management.py +221 -0
- awslabs/ccapi_mcp_server/impl/utils/__init__.py +13 -0
- awslabs/ccapi_mcp_server/impl/utils/validation.py +64 -0
- awslabs/ccapi_mcp_server/infrastructure_generator.py +160 -0
- awslabs/ccapi_mcp_server/models/__init__.py +13 -0
- awslabs/ccapi_mcp_server/models/models.py +118 -0
- awslabs/ccapi_mcp_server/schema_manager.py +219 -0
- awslabs/ccapi_mcp_server/server.py +733 -0
- awslabs/ccapi_mcp_server/static/__init__.py +13 -0
- awslabs_ccapi_mcp_server-1.0.1.dist-info/METADATA +656 -0
- awslabs_ccapi_mcp_server-1.0.1.dist-info/RECORD +28 -0
- awslabs_ccapi_mcp_server-1.0.1.dist-info/WHEEL +4 -0
- awslabs_ccapi_mcp_server-1.0.1.dist-info/entry_points.txt +2 -0
- awslabs_ccapi_mcp_server-1.0.1.dist-info/licenses/LICENSE +175 -0
- awslabs_ccapi_mcp_server-1.0.1.dist-info/licenses/NOTICE +2 -0
awslabs/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# This file is part of the awslabs namespace.
|
|
16
|
+
# It is intentionally minimal to support PEP 420 namespace packages.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""awslabs.ccapi-mcp-server"""
|
|
16
|
+
|
|
17
|
+
__version__ = '1.0.1'
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import botocore.config
|
|
16
|
+
from awslabs.ccapi_mcp_server import __version__
|
|
17
|
+
from awslabs.ccapi_mcp_server.errors import ClientError
|
|
18
|
+
from boto3 import Session
|
|
19
|
+
from os import environ
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
session_config = botocore.config.Config(
|
|
23
|
+
user_agent_extra=f'ccapi-mcp-server/{__version__}',
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_aws_client(service_name, region_name=None):
|
|
28
|
+
"""Create and return an AWS service client using boto3's default credential chain.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
service_name: AWS service name (e.g., 'cloudcontrol', 'logs', 'marketplace-catalog')
|
|
32
|
+
region_name: AWS region name (defaults to boto3's default region resolution)
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Boto3 client for the specified service
|
|
36
|
+
"""
|
|
37
|
+
# Handle FieldInfo objects (from pydantic)
|
|
38
|
+
if hasattr(region_name, 'default') and region_name is not None:
|
|
39
|
+
region_name = region_name.default
|
|
40
|
+
|
|
41
|
+
# AWS Region Resolution Order (highest to lowest priority):
|
|
42
|
+
# 1. region_name argument passed to this function
|
|
43
|
+
# 2. AWS_REGION environment variable
|
|
44
|
+
# 3. region setting in ~/.aws/config for the active profile
|
|
45
|
+
# 4. "us-east-1" as the final fallback
|
|
46
|
+
#
|
|
47
|
+
# This follows boto3's standard region resolution chain, ensuring consistent
|
|
48
|
+
# behavior with other AWS tools and SDKs.
|
|
49
|
+
profile_name = environ.get('AWS_PROFILE')
|
|
50
|
+
session = Session(profile_name=profile_name) if profile_name else Session()
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
return session.client(service_name, region_name=region_name, config=session_config)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
if 'ExpiredToken' in str(e):
|
|
56
|
+
raise ClientError('Your AWS credentials have expired. Please refresh them.')
|
|
57
|
+
elif 'NoCredentialProviders' in str(e):
|
|
58
|
+
raise ClientError(
|
|
59
|
+
'No AWS credentials found. Please configure credentials using environment variables or AWS configuration.'
|
|
60
|
+
)
|
|
61
|
+
else:
|
|
62
|
+
raise ClientError('Got an error when loading your client.')
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from awslabs.ccapi_mcp_server.errors import ClientError
|
|
16
|
+
from typing import Any, Dict
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def add_default_tags(properties: Dict, schema: Dict) -> Dict:
|
|
20
|
+
"""Add default tags to resource properties. Always tries to add tags - let AWS reject if unsupported."""
|
|
21
|
+
# Return empty dict when properties is None or empty dict {}
|
|
22
|
+
# This prevents processing invalid/missing resource properties
|
|
23
|
+
if not properties:
|
|
24
|
+
return {}
|
|
25
|
+
|
|
26
|
+
properties_with_tags = properties.copy()
|
|
27
|
+
|
|
28
|
+
# Always try to add tags - don't check schema since it can be unreliable
|
|
29
|
+
# Ensure Tags array exists
|
|
30
|
+
if 'Tags' not in properties_with_tags:
|
|
31
|
+
properties_with_tags['Tags'] = []
|
|
32
|
+
|
|
33
|
+
tags = properties_with_tags['Tags']
|
|
34
|
+
# Add default tags if they don't exist
|
|
35
|
+
managed_by_exists = any(tag.get('Key') == 'MANAGED_BY' for tag in tags)
|
|
36
|
+
source_exists = any(tag.get('Key') == 'MCP_SERVER_SOURCE_CODE' for tag in tags)
|
|
37
|
+
version_exists = any(tag.get('Key') == 'MCP_SERVER_VERSION' for tag in tags)
|
|
38
|
+
|
|
39
|
+
if not managed_by_exists:
|
|
40
|
+
tags.append({'Key': 'MANAGED_BY', 'Value': 'CCAPI-MCP-SERVER'})
|
|
41
|
+
if not source_exists:
|
|
42
|
+
tags.append(
|
|
43
|
+
{
|
|
44
|
+
'Key': 'MCP_SERVER_SOURCE_CODE',
|
|
45
|
+
'Value': 'https://github.com/awslabs/mcp/tree/main/src/ccapi-mcp-server',
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
if not version_exists:
|
|
49
|
+
from awslabs.ccapi_mcp_server import __version__
|
|
50
|
+
|
|
51
|
+
tags.append({'Key': 'MCP_SERVER_VERSION', 'Value': __version__})
|
|
52
|
+
|
|
53
|
+
properties_with_tags['Tags'] = tags
|
|
54
|
+
|
|
55
|
+
return properties_with_tags
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def validate_patch(patch_document: Any):
|
|
59
|
+
"""A best effort check that makes sure that the format of a patch document is valid before sending it to CloudControl."""
|
|
60
|
+
if not isinstance(patch_document, list):
|
|
61
|
+
raise ClientError('Patch document must be a list')
|
|
62
|
+
|
|
63
|
+
for patch_op in patch_document:
|
|
64
|
+
if not isinstance(patch_op, dict):
|
|
65
|
+
raise ClientError('Each patch operation must be a dictionary')
|
|
66
|
+
if 'op' not in patch_op:
|
|
67
|
+
raise ClientError("Each patch operation must include an 'op' field")
|
|
68
|
+
if patch_op['op'] not in ['add', 'remove', 'replace', 'move', 'copy', 'test']:
|
|
69
|
+
raise ClientError(
|
|
70
|
+
f"Operation '{patch_op['op']}' is not supported. Must be one of: add, remove, replace, move, copy, test"
|
|
71
|
+
)
|
|
72
|
+
if 'path' not in patch_op:
|
|
73
|
+
raise ClientError("Each patch operation must include a 'path' field")
|
|
74
|
+
# Value is required for add, replace, and test operations
|
|
75
|
+
if patch_op['op'] in ['add', 'replace', 'test'] and 'value' not in patch_op:
|
|
76
|
+
raise ClientError(f"The '{patch_op['op']}' operation requires a 'value' field")
|
|
77
|
+
# From is required for move and copy operations
|
|
78
|
+
if patch_op['op'] in ['move', 'copy'] and 'from' not in patch_op:
|
|
79
|
+
raise ClientError(f"The '{patch_op['op']}' operation requires a 'from' field")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def progress_event(response_event, hooks_events) -> Dict[str, Any]:
|
|
83
|
+
"""Map a CloudControl API response to a standard output format for the MCP."""
|
|
84
|
+
response = {
|
|
85
|
+
'status': response_event['OperationStatus'],
|
|
86
|
+
'resource_type': response_event['TypeName'],
|
|
87
|
+
'is_complete': response_event['OperationStatus'] == 'SUCCESS'
|
|
88
|
+
or response_event['OperationStatus'] == 'FAILED',
|
|
89
|
+
'request_token': response_event['RequestToken'],
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if response_event.get('Identifier', None):
|
|
93
|
+
response['identifier'] = response_event['Identifier']
|
|
94
|
+
if response_event.get('ResourceModel', None):
|
|
95
|
+
response['resource_info'] = response_event['ResourceModel']
|
|
96
|
+
if response_event.get('ErrorCode', None):
|
|
97
|
+
response['error_code'] = response_event['ErrorCode']
|
|
98
|
+
if response_event.get('EventTime', None):
|
|
99
|
+
response['event_time'] = response_event['EventTime']
|
|
100
|
+
if response_event.get('RetryAfter', None):
|
|
101
|
+
response['retry_after'] = response_event['RetryAfter']
|
|
102
|
+
|
|
103
|
+
# CloudControl returns a list of hooks events which may also contain a message which should
|
|
104
|
+
# take precedent over the status message returned from CloudControl directly
|
|
105
|
+
hooks_status_message = None
|
|
106
|
+
if hooks_events:
|
|
107
|
+
failed_hook_event_messages = (
|
|
108
|
+
hook_event['HookStatusMessage']
|
|
109
|
+
for hook_event in hooks_events
|
|
110
|
+
if hook_event.get('HookStatus', None) == 'HOOK_COMPLETE_FAILED'
|
|
111
|
+
or hook_event.get('HookStatus', None) == 'HOOK_FAILED'
|
|
112
|
+
)
|
|
113
|
+
hooks_status_message = next(failed_hook_event_messages, None)
|
|
114
|
+
|
|
115
|
+
if hooks_status_message:
|
|
116
|
+
response['status_message'] = hooks_status_message
|
|
117
|
+
elif response_event.get('StatusMessage', None):
|
|
118
|
+
response['status_message'] = response_event['StatusMessage']
|
|
119
|
+
|
|
120
|
+
return response
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from awslabs.ccapi_mcp_server.errors import ServerError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Context:
|
|
19
|
+
"""A singleton which includes context for the MCP server such as startup parameters."""
|
|
20
|
+
|
|
21
|
+
_instance = None
|
|
22
|
+
|
|
23
|
+
def __init__(self, readonly_mode: bool):
|
|
24
|
+
"""Initializes the context."""
|
|
25
|
+
self._readonly_mode = readonly_mode
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def readonly_mode(cls) -> bool:
|
|
29
|
+
"""If a the server was started up with the argument --readonly True, this will be set to True."""
|
|
30
|
+
if cls._instance is None:
|
|
31
|
+
raise ServerError('Context was not initialized')
|
|
32
|
+
return cls._instance._readonly_mode
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def initialize(cls, readonly_mode: bool):
|
|
36
|
+
"""Create the singleton instance of the type."""
|
|
37
|
+
cls._instance = cls(readonly_mode)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def handle_aws_api_error(e: Exception) -> Exception:
|
|
17
|
+
"""Handle AWS API errors using boto3's built-in exception information.
|
|
18
|
+
|
|
19
|
+
Leverages boto3's structured error handling as documented at:
|
|
20
|
+
https://boto3.amazonaws.com/v1/documentation/api/latest/guide/error-handling.html
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
e: The exception that was raised
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Standardized ClientError with AWS error details
|
|
27
|
+
"""
|
|
28
|
+
# Import boto3 exceptions for proper type checking
|
|
29
|
+
from botocore.exceptions import ClientError as BotocoreClientError
|
|
30
|
+
from botocore.exceptions import NoCredentialsError, PartialCredentialsError
|
|
31
|
+
|
|
32
|
+
# Handle boto3's structured exceptions directly
|
|
33
|
+
if isinstance(e, BotocoreClientError):
|
|
34
|
+
# boto3 ClientError already has structured error information
|
|
35
|
+
error_code = e.response['Error']['Code']
|
|
36
|
+
error_message = e.response['Error']['Message']
|
|
37
|
+
return ClientError(f'AWS API Error ({error_code}): {error_message}')
|
|
38
|
+
elif isinstance(e, NoCredentialsError):
|
|
39
|
+
return ClientError('AWS credentials not found. Please configure your AWS credentials.')
|
|
40
|
+
elif isinstance(e, PartialCredentialsError):
|
|
41
|
+
return ClientError(
|
|
42
|
+
'Incomplete AWS credentials. Please check your AWS credential configuration.'
|
|
43
|
+
)
|
|
44
|
+
else:
|
|
45
|
+
# Fallback for other exceptions
|
|
46
|
+
return ClientError(f'An error occurred: {str(e)}')
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ClientError(Exception):
|
|
50
|
+
"""An error that indicates that the request was malformed or incorrect in some way. There was no issue on the server side."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, message):
|
|
53
|
+
"""Call super and set message."""
|
|
54
|
+
# Call the base class constructor with the parameters it needs
|
|
55
|
+
super().__init__(message)
|
|
56
|
+
self.type = 'client'
|
|
57
|
+
self.message = message
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ServerError(Exception):
|
|
61
|
+
"""An error that indicates that there was an issue processing the request."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, message: str):
|
|
64
|
+
"""Initialize ServerError with message."""
|
|
65
|
+
super().__init__(message)
|
|
66
|
+
self.type = 'server'
|
|
67
|
+
self.message = message
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""CloudFormation IaC Generator tool implementation."""
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
from awslabs.ccapi_mcp_server.aws_client import get_aws_client
|
|
19
|
+
from awslabs.ccapi_mcp_server.errors import ClientError, handle_aws_api_error
|
|
20
|
+
from typing import Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def create_template(
|
|
24
|
+
template_name: Optional[str] = None,
|
|
25
|
+
resources: Optional[List[Dict[str, str]]] = None,
|
|
26
|
+
output_format: str = 'YAML',
|
|
27
|
+
deletion_policy: str = 'RETAIN',
|
|
28
|
+
update_replace_policy: str = 'RETAIN',
|
|
29
|
+
template_id: Optional[str] = None,
|
|
30
|
+
save_to_file: Optional[str] = None,
|
|
31
|
+
region_name: Optional[str] = None,
|
|
32
|
+
) -> Dict:
|
|
33
|
+
"""Create a CloudFormation template from existing resources using the IaC Generator API.
|
|
34
|
+
|
|
35
|
+
This function handles three main scenarios:
|
|
36
|
+
1. Starting a new template generation process
|
|
37
|
+
2. Checking the status of an existing template generation process
|
|
38
|
+
3. Retrieving a generated template
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
template_name: Name for the generated template
|
|
42
|
+
resources: List of resources to include in the template, each with 'ResourceType' and 'ResourceIdentifier'
|
|
43
|
+
output_format: Output format for the template (JSON or YAML)
|
|
44
|
+
deletion_policy: Default DeletionPolicy for resources in the template (RETAIN, DELETE, or SNAPSHOT)
|
|
45
|
+
update_replace_policy: Default UpdateReplacePolicy for resources in the template (RETAIN, DELETE, or SNAPSHOT)
|
|
46
|
+
template_id: ID of an existing template generation process to check status or retrieve template
|
|
47
|
+
save_to_file: Path to save the generated template to a file
|
|
48
|
+
region_name: AWS region name
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
A dictionary containing information about the template generation process or the generated template
|
|
52
|
+
"""
|
|
53
|
+
# Validate parameters
|
|
54
|
+
if not template_id and not template_name:
|
|
55
|
+
raise ClientError('Either template_name or template_id must be provided')
|
|
56
|
+
|
|
57
|
+
if output_format not in ['JSON', 'YAML']:
|
|
58
|
+
raise ClientError("output_format must be either 'JSON' or 'YAML'")
|
|
59
|
+
|
|
60
|
+
if deletion_policy not in ['RETAIN', 'DELETE', 'SNAPSHOT']:
|
|
61
|
+
raise ClientError("deletion_policy must be one of 'RETAIN', 'DELETE', or 'SNAPSHOT'")
|
|
62
|
+
|
|
63
|
+
if update_replace_policy not in ['RETAIN', 'DELETE', 'SNAPSHOT']:
|
|
64
|
+
raise ClientError("update_replace_policy must be one of 'RETAIN', 'DELETE', or 'SNAPSHOT'")
|
|
65
|
+
|
|
66
|
+
# Get CloudFormation client
|
|
67
|
+
cfn_client = get_aws_client('cloudformation', region_name)
|
|
68
|
+
|
|
69
|
+
# Case 1: Check status or retrieve template for an existing template generation process
|
|
70
|
+
if template_id:
|
|
71
|
+
return await _handle_existing_template(
|
|
72
|
+
cfn_client, template_id, save_to_file, output_format
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Case 2: Start a new template generation process
|
|
76
|
+
return await _start_template_generation(
|
|
77
|
+
cfn_client, template_name, resources, deletion_policy, update_replace_policy
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def _start_template_generation(
|
|
82
|
+
cfn_client,
|
|
83
|
+
template_name: str | None,
|
|
84
|
+
resources: Optional[List[Dict[str, str]]],
|
|
85
|
+
deletion_policy: str,
|
|
86
|
+
update_replace_policy: str,
|
|
87
|
+
) -> Dict:
|
|
88
|
+
"""Start a new template generation process.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
cfn_client: Boto3 CloudFormation client
|
|
92
|
+
template_name: Name for the generated template
|
|
93
|
+
resources: List of resources to include in the template
|
|
94
|
+
output_format: Output format for the template (JSON or YAML)
|
|
95
|
+
deletion_policy: DeletionPolicy for resources in the template
|
|
96
|
+
update_replace_policy: UpdateReplacePolicy for resources in the template
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
A dictionary containing information about the template generation process
|
|
100
|
+
"""
|
|
101
|
+
# Prepare parameters for the API call
|
|
102
|
+
params = {
|
|
103
|
+
'GeneratedTemplateName': template_name,
|
|
104
|
+
'TemplateConfiguration': {
|
|
105
|
+
'DeletionPolicy': deletion_policy,
|
|
106
|
+
'UpdateReplacePolicy': update_replace_policy,
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Add resources if provided
|
|
111
|
+
if resources:
|
|
112
|
+
resource_identifiers = []
|
|
113
|
+
for resource in resources:
|
|
114
|
+
if 'ResourceType' not in resource or 'ResourceIdentifier' not in resource:
|
|
115
|
+
raise ClientError(
|
|
116
|
+
"Each resource must have 'ResourceType' and 'ResourceIdentifier'"
|
|
117
|
+
)
|
|
118
|
+
resource_identifiers.append(
|
|
119
|
+
{
|
|
120
|
+
'ResourceType': resource['ResourceType'],
|
|
121
|
+
'ResourceIdentifier': resource['ResourceIdentifier'],
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
params['Resources'] = resource_identifiers
|
|
125
|
+
|
|
126
|
+
# Call the API
|
|
127
|
+
try:
|
|
128
|
+
response = cfn_client.create_generated_template(**params)
|
|
129
|
+
return {
|
|
130
|
+
'status': 'INITIATED',
|
|
131
|
+
'template_id': response['GeneratedTemplateId'],
|
|
132
|
+
'message': 'Template generation initiated. Use the template_id to check status.',
|
|
133
|
+
}
|
|
134
|
+
except Exception as e:
|
|
135
|
+
raise handle_aws_api_error(e)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
async def _handle_existing_template(
|
|
139
|
+
cfn_client, template_id: str, save_to_file: Optional[str], output_format: str = 'YAML'
|
|
140
|
+
) -> Dict:
|
|
141
|
+
"""Handle an existing template generation process - check status or retrieve template.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
cfn_client: Boto3 CloudFormation client
|
|
145
|
+
template_id: ID of the template generation process
|
|
146
|
+
save_to_file: Path to save the generated template to a file
|
|
147
|
+
output_format: Format of generated template. Either JSON or YAML
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
A dictionary containing information about the template generation process or the generated template
|
|
151
|
+
"""
|
|
152
|
+
# Check the status of the template generation process
|
|
153
|
+
try:
|
|
154
|
+
status_response = cfn_client.describe_generated_template(GeneratedTemplateName=template_id)
|
|
155
|
+
|
|
156
|
+
status = status_response['Status']
|
|
157
|
+
|
|
158
|
+
# Return status information if the template is not yet complete
|
|
159
|
+
if status != 'COMPLETE':
|
|
160
|
+
return {
|
|
161
|
+
'status': status,
|
|
162
|
+
'template_id': template_id,
|
|
163
|
+
'message': f'Template generation {status.lower()}.',
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# If the template is complete, retrieve it
|
|
167
|
+
template_response = cfn_client.get_generated_template(
|
|
168
|
+
GeneratedTemplateName=template_id, Format=output_format
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
template_content = template_response['TemplateBody']
|
|
172
|
+
resources = status_response.get('ResourceIdentifiers', [])
|
|
173
|
+
|
|
174
|
+
# Save the template to a file if requested
|
|
175
|
+
file_path = None
|
|
176
|
+
if save_to_file:
|
|
177
|
+
try:
|
|
178
|
+
# Ensure the directory exists
|
|
179
|
+
os.makedirs(os.path.dirname(os.path.abspath(save_to_file)), exist_ok=True)
|
|
180
|
+
|
|
181
|
+
# Write the template to the file
|
|
182
|
+
with open(save_to_file, 'w') as f:
|
|
183
|
+
f.write(template_content)
|
|
184
|
+
file_path = save_to_file
|
|
185
|
+
except Exception as e:
|
|
186
|
+
raise ClientError(f'Failed to save template to file: {str(e)}')
|
|
187
|
+
|
|
188
|
+
# Return the template and related information
|
|
189
|
+
result = {
|
|
190
|
+
'status': 'COMPLETED',
|
|
191
|
+
'template_id': template_id,
|
|
192
|
+
'template': template_content,
|
|
193
|
+
'resources': resources,
|
|
194
|
+
'message': 'Template generation completed.',
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if file_path:
|
|
198
|
+
result['file_path'] = file_path
|
|
199
|
+
|
|
200
|
+
return result
|
|
201
|
+
|
|
202
|
+
except Exception as e:
|
|
203
|
+
raise handle_aws_api_error(e)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|