awslabs.finch-mcp-server 0.1.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.
awslabs/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
4
+ # with the License. A copy of the License is located at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
9
+ # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
10
+ # and limitations under the License.
11
+
12
+ """AWSLabs package for MCP servers."""
@@ -0,0 +1,14 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
4
+ # with the License. A copy of the License is located at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
9
+ # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
10
+ # and limitations under the License.
11
+
12
+ """awslabs.finch-mcp-server"""
13
+
14
+ __version__ = '0.1.0'
@@ -0,0 +1,40 @@
1
+ """Constants for the Finch MCP server.
2
+
3
+ This module defines constants used throughout the Finch MCP server.
4
+ """
5
+
6
+ import os
7
+ import sys
8
+
9
+
10
+ # Server name
11
+ SERVER_NAME = 'finch_mcp_server'
12
+
13
+ # Log file name
14
+ LOG_FILE = 'finch_server.log'
15
+
16
+ # VM states
17
+ VM_STATE_RUNNING = 'running'
18
+ VM_STATE_STOPPED = 'stopped'
19
+ VM_STATE_NONEXISTENT = 'nonexistent'
20
+ VM_STATE_UNKNOWN = 'unknown'
21
+
22
+ # Operation status
23
+ STATUS_SUCCESS = 'success'
24
+ STATUS_ERROR = 'error'
25
+ STATUS_WARNING = 'warning'
26
+ STATUS_INFO = 'info'
27
+
28
+ # AWS region pattern
29
+ REGION_PATTERN = r'^[a-zA-Z0-9][a-zA-Z0-9-_]*$'
30
+
31
+ # ECR repository pattern
32
+ ECR_REFERENCE_PATTERN = r'(\d{12})\.dkr[-.]ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.(on\.aws|amazonaws\.com(\.cn)?|sc2s\.sgov\.gov|c2s\.ic\.gov|cloud\.adc-e\.uk|csp\.hci\.ic\.gov)'
33
+
34
+ # Platform-specific configuration file paths
35
+ if sys.platform == 'win32':
36
+ # Windows path using %LocalAppData%
37
+ FINCH_YAML_PATH = os.path.join(os.environ.get('LOCALAPPDATA', ''), '.finch', 'finch.yaml')
38
+ else:
39
+ # macOS path
40
+ FINCH_YAML_PATH = '~/.finch/finch.yaml'
@@ -0,0 +1,19 @@
1
+ """Pydantic models for the Finch MCP server.
2
+
3
+ This module defines the data models used for request and response validation
4
+ in the Finch MCP server tools.
5
+ """
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class Result(BaseModel):
11
+ """Base model for operation results.
12
+
13
+ This model only includes status and message fields, regardless of what additional
14
+ fields might be present in the input dictionary. This ensures that only these two
15
+ fields are returned to the user.
16
+ """
17
+
18
+ status: str = Field(..., description="Status of the operation ('success', 'error', etc.)")
19
+ message: str = Field(..., description='Descriptive message about the result of the operation')
@@ -0,0 +1,442 @@
1
+ """Finch MCP Server main module.
2
+
3
+ This module provides the MCP server implementation for Finch container operations.
4
+
5
+ Note: The tools provided by this MCP server are intended for development and prototyping
6
+ purposes only and are not meant for production use cases.
7
+ """
8
+
9
+ import os
10
+ import re
11
+ import sys
12
+ from awslabs.finch_mcp_server.consts import LOG_FILE, SERVER_NAME
13
+
14
+ # Import Pydantic models for input validation
15
+ from awslabs.finch_mcp_server.models import Result
16
+ from awslabs.finch_mcp_server.utils.build import build_image, contains_ecr_reference
17
+ from awslabs.finch_mcp_server.utils.common import format_result
18
+ from awslabs.finch_mcp_server.utils.ecr import create_ecr_repository
19
+
20
+ # Import utility functions from local modules
21
+ from awslabs.finch_mcp_server.utils.push import is_ecr_repository, push_image
22
+ from awslabs.finch_mcp_server.utils.vm import (
23
+ check_finch_installation,
24
+ configure_ecr,
25
+ get_vm_status,
26
+ initialize_vm,
27
+ is_vm_nonexistent,
28
+ is_vm_running,
29
+ is_vm_stopped,
30
+ start_stopped_vm,
31
+ stop_vm,
32
+ )
33
+ from loguru import logger
34
+ from mcp.server.fastmcp import FastMCP
35
+ from pydantic import Field
36
+ from typing import Any, Dict, List, Optional
37
+
38
+
39
+ # Configure loguru logger
40
+ def sensitive_data_filter(record):
41
+ """Filter that redacts sensitive information from log messages.
42
+
43
+ This function processes log records to redact sensitive information such as
44
+ API keys, passwords, and credentials from the message.
45
+
46
+ Args:
47
+ record: The log record to process
48
+
49
+ Returns:
50
+ bool: True to allow the log record to be processed, False to filter it out
51
+
52
+ """
53
+ # Define patterns for sensitive data detection
54
+ patterns = [
55
+ # AWS Access Key (20 character alphanumeric)
56
+ (re.compile(r'((?<![A-Z0-9])[A-Z0-9]{20}(?![A-Z0-9]))'), 'AWS_ACCESS_KEY_REDACTED'),
57
+ # AWS Secret Key (40 character base64)
58
+ (
59
+ re.compile(r'((?<![A-Za-z0-9/+=])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=]))'),
60
+ 'AWS_SECRET_KEY_REDACTED',
61
+ ),
62
+ # API Keys
63
+ (
64
+ re.compile(r'(api[_-]?key[=:]\s*[\'"]?)[^\'"\s]+([\'"]?)', re.IGNORECASE),
65
+ r'api_key=REDACTED',
66
+ ),
67
+ # Passwords
68
+ (
69
+ re.compile(r'(password[=:]\s*[\'"]?)[^\'"\s]+([\'"]?)', re.IGNORECASE),
70
+ r'password=REDACTED',
71
+ ),
72
+ # Secrets
73
+ (
74
+ re.compile(r'(secret[=:]\s*[\'"]?)[^\'"\s]+([\'"]?)', re.IGNORECASE),
75
+ r'secret=REDACTED',
76
+ ),
77
+ # Tokens
78
+ (re.compile(r'(token[=:]\s*[\'"]?)[^\'"\s]+([\'"]?)', re.IGNORECASE), r'\1REDACTED\2'),
79
+ # URLs with credentials
80
+ (re.compile(r'(https?://)([^:@\s]+):([^:@\s]+)@'), r'\1REDACTED:REDACTED@'),
81
+ # JWT tokens (common format)
82
+ (
83
+ re.compile(r'eyJ[a-zA-Z0-9_-]{5,}\.eyJ[a-zA-Z0-9_-]{5,}\.[a-zA-Z0-9_-]{5,}'),
84
+ 'JWT_TOKEN_REDACTED',
85
+ ),
86
+ # OAuth tokens
87
+ (
88
+ re.compile(r'(oauth[_-]?token[=:]\s*[\'"]?)[^\'"\s]+([\'"]?)', re.IGNORECASE),
89
+ r'\1REDACTED\2',
90
+ ),
91
+ # Generic credentials
92
+ (
93
+ re.compile(r'(credential[s]?[=:]\s*[\'"]?)[^\'"\s]+([\'"]?)', re.IGNORECASE),
94
+ r'\1REDACTED\2',
95
+ ),
96
+ ]
97
+
98
+ try:
99
+ if 'message' in record:
100
+ message = record['message']
101
+
102
+ for pattern, replacement in patterns:
103
+ message = pattern.sub(replacement, message)
104
+
105
+ record['message'] = message
106
+
107
+ except Exception as e:
108
+ if 'message' in record:
109
+ record['message'] = (
110
+ f'{record["message"]} [SENSITIVE_DATA_FILTER_ERROR: Exception occurred during sensitive data filtering]'
111
+ )
112
+ else:
113
+ record['message'] = (
114
+ '[SENSITIVE_DATA_FILTER_ERROR: Exception occurred during sensitive data filtering]'
115
+ )
116
+ logger.debug(f'Error in sensitive_data_filter: {str(e)}')
117
+
118
+ # Return True to allow the log record to be processed
119
+ return True
120
+
121
+
122
+ # Remove all default handlers then add our own
123
+ logger.remove()
124
+
125
+ log_level = os.environ.get('FASTMCP_LOG_LEVEL', 'INFO').upper()
126
+ logger.add(
127
+ LOG_FILE,
128
+ rotation='10 MB',
129
+ retention=7,
130
+ level=log_level,
131
+ format='{time:YYYY-MM-DD HH:mm:ss} | {level} | {name}:{function}:{line} | {message}',
132
+ filter=sensitive_data_filter,
133
+ )
134
+
135
+ # Add a handler for stderr
136
+ logger.add(
137
+ sys.stderr,
138
+ level=log_level,
139
+ format='{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}',
140
+ filter=sensitive_data_filter,
141
+ )
142
+
143
+ logger = logger.bind(name=SERVER_NAME)
144
+
145
+ # Initialize the MCP server
146
+ mcp = FastMCP(SERVER_NAME)
147
+ enable_aws_resource_write = False
148
+
149
+
150
+ def ensure_vm_running() -> Dict[str, Any]:
151
+ """Ensure that the Finch VM is running before performing operations.
152
+
153
+ This function checks the current status of the Finch VM and takes appropriate action:
154
+ - If the VM is nonexistent: Creates a new VM instance using 'finch vm init'
155
+ - If the VM is stopped: Starts the VM using 'finch vm start'
156
+ - If the VM is already running: Does nothing
157
+
158
+ Returns:
159
+ Dict[str, Any]: A dictionary containing:
160
+ - status (str): "success" if the VM is running or was started successfully,
161
+ "error" otherwise
162
+ - message (str): A descriptive message about the result of the operation
163
+
164
+ """
165
+ try:
166
+ if sys.platform == 'linux':
167
+ logger.info('Linux OS detected. Finch does not use a VM on Linux...')
168
+ return format_result('success', 'Finch does not use a VM on Linux..')
169
+
170
+ status_result = get_vm_status()
171
+
172
+ if is_vm_nonexistent(status_result):
173
+ logger.info('Finch VM does not exist. Initializing...')
174
+ result = initialize_vm()
175
+ if result['status'] == 'error':
176
+ return result
177
+ return format_result('success', 'Finch VM was initialized successfully.')
178
+ elif is_vm_stopped(status_result):
179
+ logger.info('Finch VM is stopped. Starting it...')
180
+ result = start_stopped_vm()
181
+ if result['status'] == 'error':
182
+ return result
183
+ return format_result('success', 'Finch VM was started successfully.')
184
+ elif is_vm_running(status_result):
185
+ return format_result('success', 'Finch VM is already running.')
186
+ else:
187
+ return format_result(
188
+ 'error',
189
+ f'Unknown VM status: status code {status_result.returncode}',
190
+ )
191
+ except Exception as e:
192
+ return format_result('error', f'Error ensuring Finch VM is running: {str(e)}')
193
+
194
+
195
+ @mcp.tool()
196
+ async def finch_build_container_image(
197
+ dockerfile_path: str = Field(..., description='Absolute path to the Dockerfile'),
198
+ context_path: str = Field(..., description='Absolute path to the build context directory'),
199
+ tags: Optional[List[str]] = Field(
200
+ default=None,
201
+ description="List of tags to apply to the image (e.g., ['myimage:latest', 'myimage:v1'])",
202
+ ),
203
+ platforms: Optional[List[str]] = Field(
204
+ default=None, description="List of target platforms (e.g., ['linux/amd64', 'linux/arm64'])"
205
+ ),
206
+ target: Optional[str] = Field(default=None, description='Target build stage to build'),
207
+ no_cache: Optional[bool] = Field(default=False, description='Whether to disable cache'),
208
+ pull: Optional[bool] = Field(default=False, description='Whether to always pull base images'),
209
+ build_contexts: Optional[List[str]] = Field(
210
+ default=None, description='List of additional build contexts'
211
+ ),
212
+ outputs: Optional[str] = Field(default=None, description='Output destination'),
213
+ cache_from: Optional[List[str]] = Field(
214
+ default=None, description='List of external cache sources'
215
+ ),
216
+ quiet: Optional[bool] = Field(default=False, description='Whether to suppress build output'),
217
+ progress: Optional[str] = Field(default='auto', description='Type of progress output'),
218
+ ) -> Result:
219
+ """Build a container image using Finch.
220
+
221
+ This tool builds a Docker image using the specified Dockerfile and context directory.
222
+ It supports a range of build options including tags, platforms, and more.
223
+ If the Dockerfile contains references to ECR repositories, it verifies that
224
+ ecr login cred helper is properly configured before proceeding with the build.
225
+
226
+ Note: for ecr-login to work server needs access to AWS credentials/profile which are configured
227
+ in the server mcp configuration file.
228
+
229
+ Returns:
230
+ Result: An object containing:
231
+ - status (str): "success" if the operation succeeded, "error" otherwise
232
+ - message (str): A descriptive message about the result of the operation
233
+
234
+ Example response:
235
+ Result(status="success", message="Successfully built image from /path/to/Dockerfile")
236
+
237
+ """
238
+ logger.info('tool-name: finch_build_container_image')
239
+ logger.info(f'tool-args: dockerfile_path={dockerfile_path}, context_path={context_path}')
240
+
241
+ try:
242
+ finch_install_status = check_finch_installation()
243
+ if finch_install_status['status'] == 'error':
244
+ return Result(**finch_install_status)
245
+
246
+ if contains_ecr_reference(dockerfile_path):
247
+ logger.info('ECR reference detected in Dockerfile, configuring ECR login')
248
+ config_result, config_changed = configure_ecr()
249
+ if config_result['status'] == 'error':
250
+ return Result(**config_result)
251
+ if config_changed:
252
+ logger.info('ECR configuration changed, restarting VM')
253
+ stop_vm(force=True)
254
+
255
+ vm_status = ensure_vm_running()
256
+ if vm_status['status'] == 'error':
257
+ return Result(**vm_status)
258
+
259
+ result = build_image(
260
+ dockerfile_path=dockerfile_path,
261
+ context_path=context_path,
262
+ tags=tags,
263
+ platforms=platforms,
264
+ target=target,
265
+ no_cache=no_cache,
266
+ pull=pull,
267
+ build_contexts=build_contexts,
268
+ outputs=outputs,
269
+ cache_from=cache_from,
270
+ quiet=quiet,
271
+ progress=progress,
272
+ )
273
+ return Result(**result)
274
+ except Exception as e:
275
+ error_result = format_result('error', f'Error building Docker image: {str(e)}')
276
+ return Result(**error_result)
277
+
278
+
279
+ @mcp.tool()
280
+ async def finch_push_image(
281
+ image: str = Field(
282
+ ..., description='The full image name to push, including the repository URL and tag'
283
+ ),
284
+ ) -> Result:
285
+ """Push a container image to a repository using finch, replacing the tag with the image hash.
286
+
287
+ If the image URL is an ECR repository, it verifies that ECR login cred helper is configured.
288
+ This tool gets the image hash, creates a new tag using the hash, and pushes the image with
289
+ the hash tag to the repository. If the image URL is an ECR repository, it verifies that
290
+ ECR login is properly configured before proceeding with the push.
291
+
292
+ The tool expects the image to be already built and available locally. It uses
293
+ 'finch image inspect' to get the hash, 'finch image tag' to create a new tag,
294
+ and 'finch image push' to perform the actual push operation.
295
+
296
+ When the server is in read-only mode (which is the default unless --enable-aws-resource-write
297
+ is specified), this tool will return an error when pushing to ECR repositories.
298
+
299
+ Returns:
300
+ Result: An object containing:
301
+ - status (str): "success" if the operation succeeded, "error" otherwise
302
+ - message (str): A descriptive message about the result of the operation
303
+
304
+ Example response:
305
+ Result(status="success", message="Successfully pushed image 123456789012.dkr.ecr.us-west-2.amazonaws.com/my-repo:abcdef123456 to ECR.")
306
+
307
+ """
308
+ logger.info('tool-name: finch_push_image')
309
+ logger.info(f'tool-args: image={image}')
310
+
311
+ try:
312
+ finch_install_status = check_finch_installation()
313
+ if finch_install_status['status'] == 'error':
314
+ return Result(**finch_install_status)
315
+
316
+ is_ecr = is_ecr_repository(image)
317
+ if is_ecr:
318
+ # Check if AWS resource write is enabled for ECR pushes
319
+ if not enable_aws_resource_write:
320
+ logger.warning(
321
+ f'Attempt to push image to ECR "{image}" without AWS resource write enabled'
322
+ )
323
+ error_result = format_result(
324
+ 'error', 'Server running in read-only mode, unable to push to ECR repository'
325
+ )
326
+ return Result(**error_result)
327
+
328
+ logger.info('ECR repository detected, configuring ECR login')
329
+ config_result, config_changed = configure_ecr()
330
+ if config_result['status'] == 'error':
331
+ return Result(**config_result)
332
+ if config_changed:
333
+ logger.info('ECR configuration changed, restarting VM')
334
+ stop_vm(force=True)
335
+
336
+ vm_status = ensure_vm_running()
337
+ if vm_status['status'] == 'error':
338
+ return Result(**vm_status)
339
+
340
+ result = push_image(image)
341
+ return Result(**result)
342
+ except Exception as e:
343
+ error_result = format_result('error', f'Error pushing image: {str(e)}')
344
+ return Result(**error_result)
345
+
346
+
347
+ def set_enable_aws_resource_write(enabled: bool):
348
+ """Set whether AWS resource creation/modification is enabled.
349
+
350
+ When AWS resource write is disabled, certain operations like creating ECR repositories
351
+ will return an error.
352
+
353
+ Args:
354
+ enabled (bool): True to enable AWS resource creation/modification, False to disable it
355
+
356
+ """
357
+ global enable_aws_resource_write
358
+ enable_aws_resource_write = enabled
359
+ logger.info(f'AWS resource write enabled: {enable_aws_resource_write}')
360
+
361
+
362
+ @mcp.tool()
363
+ async def finch_create_ecr_repo(
364
+ repository_name: str = Field(
365
+ ..., description='The name of the repository to check or create in ECR'
366
+ ),
367
+ region: Optional[str] = Field(
368
+ default=None,
369
+ description='AWS region for the ECR repository. If not provided, uses the default region from AWS configuration',
370
+ ),
371
+ ) -> Result:
372
+ """Check if an ECR repository exists and create it if it doesn't.
373
+
374
+ This tool checks if the specified ECR repository exists using boto3.
375
+ If the repository doesn't exist, it creates a new one with the given name.
376
+ The tool requires appropriate AWS credentials configured.
377
+
378
+ When the server is in read-only mode (which is the default unless --enable-aws-resource-write
379
+ is specified), this tool will return an error and will not create any repositories.
380
+
381
+ Returns:
382
+ Result: An object containing:
383
+ - status (str): "success" if the operation succeeded, "error" otherwise
384
+ - message (str): A descriptive message about the result of the operation
385
+
386
+ Example response:
387
+ Result(status="success", message="Successfully created ECR repository 'my-app'.",
388
+ repository_uri="123456789012.dkr.ecr.us-west-2.amazonaws.com/my-app",
389
+ exists=False)
390
+
391
+ """
392
+ logger.info('tool-name: finch_create_ecr_repo')
393
+ logger.info(f'tool-args: repository_name={repository_name}')
394
+
395
+ # Check if AWS resource write is enabled
396
+ if not enable_aws_resource_write:
397
+ logger.warning(
398
+ f'Attempt to create ECR repo "{repository_name}" without AWS resource write enabled'
399
+ )
400
+ error_result = format_result(
401
+ 'error', 'Server running in read-only mode, unable to perform the action'
402
+ )
403
+ return Result(**error_result)
404
+
405
+ try:
406
+ result = create_ecr_repository(
407
+ repository_name=repository_name,
408
+ region=region,
409
+ )
410
+ return Result(**result)
411
+ except Exception as e:
412
+ error_result = format_result('error', f'Error checking/creating ECR repository: {str(e)}')
413
+ return Result(**error_result)
414
+
415
+
416
+ def main(enable_aws_resource_write: bool = False):
417
+ """Run the Finch MCP server.
418
+
419
+ Args:
420
+ enable_aws_resource_write (bool, optional): Whether to enable AWS resource creation/modification. Defaults to False.
421
+
422
+ """
423
+ # Set AWS resource write mode
424
+ set_enable_aws_resource_write(enable_aws_resource_write)
425
+
426
+ logger.info('Starting Finch MCP server')
427
+ logger.info(f'Logs will be written to: {LOG_FILE}')
428
+ mcp.run(transport='stdio')
429
+
430
+
431
+ if __name__ == '__main__':
432
+ import argparse
433
+
434
+ parser = argparse.ArgumentParser(description='Run the Finch MCP server')
435
+ parser.add_argument(
436
+ '--enable-aws-resource-write',
437
+ action='store_true',
438
+ help='Enable AWS resource creation and modification (disabled by default)',
439
+ )
440
+ args = parser.parse_args()
441
+
442
+ main(enable_aws_resource_write=args.enable_aws_resource_write)
@@ -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"). You may not use this file except in compliance
4
+ # with the License. A copy of the License is located at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
9
+ # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
10
+ # and limitations under the License.
11
+
12
+ """
13
+ Utility modules for the Finch MCP server.
14
+
15
+ This package contains utility modules for working with Finch container client.
16
+ """
@@ -0,0 +1,141 @@
1
+ """Utility functions for building container images using Finch.
2
+
3
+ This module provides functions to build Docker images using Finch and check
4
+ if Dockerfiles contain references to ECR repositories.
5
+
6
+ Note: These tools are intended for development and prototyping purposes only
7
+ and are not meant for production use cases.
8
+ """
9
+
10
+ import os
11
+ import re
12
+ from ..consts import ECR_REFERENCE_PATTERN, STATUS_ERROR, STATUS_SUCCESS
13
+ from .common import execute_command, format_result
14
+ from loguru import logger
15
+ from typing import Any, Dict, List, Optional
16
+
17
+
18
+ def contains_ecr_reference(dockerfile_path: str) -> bool:
19
+ """Check if a Dockerfile contains references to ECR repositories.
20
+
21
+ This function scans the Dockerfile for `FROM` or other directives
22
+ that might reference an ECR repository.
23
+
24
+ Args:
25
+ dockerfile_path (str): Path to the Dockerfile to check.
26
+
27
+ Returns:
28
+ bool: True if the Dockerfile contains ECR references, False otherwise.
29
+
30
+ """
31
+ try:
32
+ if not os.path.exists(dockerfile_path):
33
+ logger.warning(f'Dockerfile not found at {dockerfile_path}')
34
+ return False
35
+
36
+ with open(dockerfile_path, 'r') as f:
37
+ content = f.read()
38
+ return bool(re.search(ECR_REFERENCE_PATTERN, content))
39
+ except Exception as e:
40
+ logger.error(f'Error checking Dockerfile for ECR references: {str(e)}')
41
+ return False
42
+
43
+
44
+ def build_image(
45
+ dockerfile_path: str,
46
+ context_path: str,
47
+ tags: Optional[List[str]] = None,
48
+ platforms: Optional[List[str]] = None,
49
+ target: Optional[str] = None,
50
+ no_cache: Optional[bool] = False,
51
+ pull: Optional[bool] = False,
52
+ build_contexts: Optional[List[str]] = None,
53
+ outputs: Optional[str] = None,
54
+ cache_from: Optional[List[str]] = None,
55
+ quiet: Optional[bool] = False,
56
+ progress: Optional[str] = None,
57
+ ) -> Dict[str, Any]:
58
+ """Build a container image using Finch.
59
+
60
+ Args:
61
+ dockerfile_path: Path to the Dockerfile
62
+ context_path: Path to the build context directory
63
+ tags: List of tags to apply to the image
64
+ platforms: List of target platforms
65
+ target: Target build stage
66
+ no_cache: Whether to disable cache
67
+ pull: Whether to always pull base images
68
+ build_contexts: List of additional build contexts
69
+ outputs: Output destination
70
+ cache_from: List of external cache sources
71
+ quiet: Whether to suppress build output
72
+ progress: Type of progress output
73
+ Returns:
74
+ Dict[str, Any]: Result of the build operation
75
+
76
+ """
77
+ try:
78
+ # Check if Dockerfile exists
79
+ if not os.path.exists(dockerfile_path):
80
+ return format_result(STATUS_ERROR, f'Dockerfile not found at {dockerfile_path}')
81
+
82
+ if not os.path.exists(context_path):
83
+ return format_result(STATUS_ERROR, f'Context directory not found at {context_path}')
84
+
85
+ command = ['finch', 'image', 'build']
86
+
87
+ command.extend(['-f', dockerfile_path])
88
+
89
+ if tags:
90
+ for tag in tags:
91
+ command.extend(['-t', tag])
92
+
93
+ if platforms:
94
+ for platform in platforms:
95
+ command.extend(['--platform', platform])
96
+
97
+ if target:
98
+ command.extend(['--target', target])
99
+
100
+ if no_cache:
101
+ command.append('--no-cache')
102
+
103
+ if pull:
104
+ command.append('--pull')
105
+
106
+ if build_contexts:
107
+ for ctx in build_contexts:
108
+ command.extend(['--build-context', ctx])
109
+
110
+ if outputs:
111
+ command.extend(['--output', outputs])
112
+
113
+ if cache_from:
114
+ for cache in cache_from:
115
+ command.extend(['--cache-from', cache])
116
+
117
+ if quiet:
118
+ command.append('--quiet')
119
+
120
+ if progress:
121
+ command.extend(['--progress', progress])
122
+
123
+ command.append(context_path)
124
+
125
+ logger.info(f'Building image with command: {" ".join(command)}')
126
+ build_result = execute_command(command)
127
+
128
+ if build_result.returncode == 0:
129
+ # Log stdout for debugging
130
+ logger.debug(f'STDOUT from build: {build_result.stdout}')
131
+ return format_result(
132
+ STATUS_SUCCESS, f'Successfully built image from {dockerfile_path}'
133
+ )
134
+ else:
135
+ # Log stderr for debugging
136
+ logger.debug(f'STDERR from build: {build_result.stderr}')
137
+ return format_result(STATUS_ERROR, f'Failed to build image: {build_result.stderr}')
138
+
139
+ except Exception as e:
140
+ logger.error(f'Error building image: {str(e)}')
141
+ return format_result(STATUS_ERROR, f'Error building image: {str(e)}')