From cc60b39039144e32de7b0fcde2af1f79dc3b1462 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 04:26:36 +0000 Subject: [PATCH 1/3] Initial plan From 2352bfb13f96d8758f3d4241fe9391dc220527b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 04:30:39 +0000 Subject: [PATCH 2/3] Add teardown.py and error handling to resources.py Co-authored-by: Karthik777 <7102951+Karthik777@users.noreply.github.com> --- fastops/__init__.py | 1 + fastops/resources.py | 468 ++++++++++++++++++++++++++++--------------- fastops/teardown.py | 433 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 746 insertions(+), 156 deletions(-) create mode 100644 fastops/teardown.py diff --git a/fastops/__init__.py b/fastops/__init__.py index 3e5abf6..3ca2cc6 100644 --- a/fastops/__init__.py +++ b/fastops/__init__.py @@ -11,4 +11,5 @@ from .compliance import * from .secrets import * from .resources import * +from .teardown import * from .ship import * \ No newline at end of file diff --git a/fastops/resources.py b/fastops/resources.py index b562372..639cb2a 100644 --- a/fastops/resources.py +++ b/fastops/resources.py @@ -5,9 +5,21 @@ import os import json import subprocess +import shutil from pathlib import Path +def _check_cli(cli_name): + 'Check if a CLI tool is installed and give a helpful error if not' + if shutil.which(cli_name) is None: + raise EnvironmentError( + f'{cli_name} CLI not found. Install it:\n' + f' aws → pip install awscli OR https://aws.amazon.com/cli/\n' + f' az → https://learn.microsoft.com/en-us/cli/azure/install-azure-cli\n' + f' gcloud → https://cloud.google.com/sdk/docs/install' + ) + + def database(name='db', engine='postgres', provider='docker', **kw): 'Provision a database: postgres, mysql, or mongo' password = kw.get('password', os.environ.get('DB_PASSWORD', 'secret')) @@ -68,23 +80,35 @@ def database(name='db', engine='postgres', provider='docker', **kw): return (env_dict, svc) elif provider == 'aws': + _check_cli('aws') from .aws import callaws instance_class = kw.get('instance_class', 'db.t3.micro') username = kw.get('username', 'appadmin') storage = kw.get('storage', 20) - result = callaws('rds', 'create-db-instance', - '--db-instance-identifier', name, - '--engine', engine, - '--db-instance-class', instance_class, - '--master-username', username, - '--master-user-password', password, - '--allocated-storage', str(storage), - '--no-publicly-accessible', - '--storage-encrypted') - - endpoint = result['DBInstance']['Endpoint']['Address'] - port = result['DBInstance']['Endpoint']['Port'] + try: + result = callaws('rds', 'create-db-instance', + '--db-instance-identifier', name, + '--engine', engine, + '--db-instance-class', instance_class, + '--master-username', username, + '--master-user-password', password, + '--allocated-storage', str(storage), + '--no-publicly-accessible', + '--storage-encrypted') + + endpoint = result['DBInstance']['Endpoint']['Address'] + port = result['DBInstance']['Endpoint']['Port'] + except Exception as e: + if 'DBInstanceAlreadyExists' in str(e): + # Describe existing instance instead + result = callaws('rds', 'describe-db-instances', + '--db-instance-identifier', name) + inst = result['DBInstances'][0] + endpoint = inst['Endpoint']['Address'] + port = inst['Endpoint']['Port'] + else: + raise env_dict = { 'DATABASE_URL': f'postgresql://{username}:{password}@{endpoint}:{port}/{name}', @@ -93,6 +117,7 @@ def database(name='db', engine='postgres', provider='docker', **kw): return (env_dict, None) elif provider == 'azure': + _check_cli('az') from .azure import callaz rg = kw.get('resource_group') sku = kw.get('sku', 'Standard_B1ms') @@ -100,17 +125,28 @@ def database(name='db', engine='postgres', provider='docker', **kw): storage_size = kw.get('storage_size', 32) admin_user = kw.get('admin_user', 'appadmin') - result = callaz('postgres', 'flexible-server', 'create', - '--name', name, - '--resource-group', rg, - '--sku-name', sku, - '--version', str(version), - '--storage-size', str(storage_size), - '--admin-user', admin_user, - '--admin-password', password, - '--public-access', 'None') - - host = result.get('fullyQualifiedDomainName', f'{name}.postgres.database.azure.com') + try: + result = callaz('postgres', 'flexible-server', 'create', + '--name', name, + '--resource-group', rg, + '--sku-name', sku, + '--version', str(version), + '--storage-size', str(storage_size), + '--admin-user', admin_user, + '--admin-password', password, + '--public-access', 'None') + + host = result.get('fullyQualifiedDomainName', f'{name}.postgres.database.azure.com') + except Exception as e: + if 'ResourceAlreadyExists' in str(e) or 'already exists' in str(e).lower(): + # Get existing server details + result = callaz('postgres', 'flexible-server', 'show', + '--name', name, + '--resource-group', rg) + host = result.get('fullyQualifiedDomainName', f'{name}.postgres.database.azure.com') + else: + raise + env_dict = { 'DATABASE_URL': f'postgresql://{admin_user}:{password}@{host}:5432/{name}', 'DB_PROVIDER': 'azure_postgres' @@ -138,17 +174,30 @@ def cache(name='redis', provider='docker', **kw): return (env_dict, svc) elif provider == 'aws': + _check_cli('aws') from .aws import callaws node_type = kw.get('node_type', 'cache.t3.micro') - result = callaws('elasticache', 'create-cache-cluster', - '--cache-cluster-id', name, - '--cache-node-type', node_type, - '--engine', 'redis', - '--num-cache-nodes', '1') - - endpoint = result['CacheCluster']['CacheNodes'][0]['Endpoint']['Address'] - port = result['CacheCluster']['CacheNodes'][0]['Endpoint']['Port'] + try: + result = callaws('elasticache', 'create-cache-cluster', + '--cache-cluster-id', name, + '--cache-node-type', node_type, + '--engine', 'redis', + '--num-cache-nodes', '1') + + endpoint = result['CacheCluster']['CacheNodes'][0]['Endpoint']['Address'] + port = result['CacheCluster']['CacheNodes'][0]['Endpoint']['Port'] + except Exception as e: + if 'CacheClusterAlreadyExists' in str(e): + # Describe existing cluster + result = callaws('elasticache', 'describe-cache-clusters', + '--cache-cluster-id', name, + '--show-cache-node-info') + cluster = result['CacheClusters'][0] + endpoint = cluster['CacheNodes'][0]['Endpoint']['Address'] + port = cluster['CacheNodes'][0]['Endpoint']['Port'] + else: + raise env_dict = { 'REDIS_URL': f'redis://{endpoint}:{port}', @@ -157,18 +206,29 @@ def cache(name='redis', provider='docker', **kw): return (env_dict, None) elif provider == 'azure': + _check_cli('az') from .azure import callaz rg = kw.get('resource_group') sku = kw.get('sku', 'Basic') vm_size = kw.get('vm_size', 'C0') - result = callaz('redis', 'create', - '--name', name, - '--resource-group', rg, - '--sku', sku, - '--vm-size', vm_size) - - host = result.get('hostName', f'{name}.redis.cache.windows.net') + try: + result = callaz('redis', 'create', + '--name', name, + '--resource-group', rg, + '--sku', sku, + '--vm-size', vm_size) + + host = result.get('hostName', f'{name}.redis.cache.windows.net') + except Exception as e: + if 'ResourceAlreadyExists' in str(e) or 'already exists' in str(e).lower(): + # Get existing cache + result = callaz('redis', 'show', + '--name', name, + '--resource-group', rg) + host = result.get('hostName', f'{name}.redis.cache.windows.net') + else: + raise # Get access key keys = callaz('redis', 'list-keys', @@ -206,6 +266,7 @@ def queue(name='tasks', provider='docker', **kw): return (env_dict, svc) elif provider == 'aws': + _check_cli('aws') from .aws import callaws result = callaws('sqs', 'create-queue', @@ -223,20 +284,29 @@ def queue(name='tasks', provider='docker', **kw): return (env_dict, None) elif provider == 'azure': + _check_cli('az') from .azure import callaz rg = kw.get('resource_group') namespace = kw.get('namespace', f'{name}-ns') - # Create namespace - callaz('servicebus', 'namespace', 'create', - '--name', namespace, - '--resource-group', rg) - - # Create queue - callaz('servicebus', 'queue', 'create', - '--name', name, - '--namespace-name', namespace, - '--resource-group', rg) + try: + # Create namespace + callaz('servicebus', 'namespace', 'create', + '--name', namespace, + '--resource-group', rg) + except Exception as e: + if 'ResourceAlreadyExists' not in str(e) and 'already exists' not in str(e).lower(): + raise + + try: + # Create queue + callaz('servicebus', 'queue', 'create', + '--name', name, + '--namespace-name', namespace, + '--resource-group', rg) + except Exception as e: + if 'ResourceAlreadyExists' not in str(e) and 'already exists' not in str(e).lower(): + raise # Get connection string keys = callaz('servicebus', 'namespace', 'authorization-rule', 'keys', 'list', @@ -252,15 +322,24 @@ def queue(name='tasks', provider='docker', **kw): return (env_dict, None) elif provider == 'gcp': - # Create topic - subprocess.run(['gcloud', 'pubsub', 'topics', 'create', name], - capture_output=True, text=True, check=True) - - # Create subscription - sub_name = f'{name}-sub' - subprocess.run(['gcloud', 'pubsub', 'subscriptions', 'create', sub_name, - '--topic', name], - capture_output=True, text=True, check=True) + _check_cli('gcloud') + try: + # Create topic + subprocess.run(['gcloud', 'pubsub', 'topics', 'create', name, '--quiet'], + capture_output=True, text=True, check=True) + except subprocess.CalledProcessError as e: + if 'already exists' not in e.stderr.lower(): + raise + + try: + # Create subscription + sub_name = f'{name}-sub' + subprocess.run(['gcloud', 'pubsub', 'subscriptions', 'create', sub_name, + '--topic', name, '--quiet'], + capture_output=True, text=True, check=True) + except subprocess.CalledProcessError as e: + if 'already exists' not in e.stderr.lower(): + raise env_dict = { 'QUEUE_TOPIC': name, @@ -298,15 +377,20 @@ def bucket(name, provider='docker', **kw): return (env_dict, svc) elif provider == 'aws': + _check_cli('aws') from .aws import callaws region = kw.get('region', 'us-east-1') - # Create bucket - if region == 'us-east-1': - callaws('s3api', 'create-bucket', '--bucket', name) - else: - callaws('s3api', 'create-bucket', '--bucket', name, - '--create-bucket-configuration', f'LocationConstraint={region}') + try: + # Create bucket + if region == 'us-east-1': + callaws('s3api', 'create-bucket', '--bucket', name) + else: + callaws('s3api', 'create-bucket', '--bucket', name, + '--create-bucket-configuration', f'LocationConstraint={region}') + except Exception as e: + if 'BucketAlreadyOwnedByYou' not in str(e) and 'BucketAlreadyExists' not in str(e): + raise # Enable encryption callaws('s3api', 'put-bucket-encryption', @@ -333,21 +417,30 @@ def bucket(name, provider='docker', **kw): return (env_dict, None) elif provider == 'azure': + _check_cli('az') from .azure import callaz rg = kw.get('resource_group') account_name = kw.get('account_name', name.replace('-', '').replace('_', '')[:24]) - # Create storage account - callaz('storage', 'account', 'create', - '--name', account_name, - '--resource-group', rg, - '--encryption-services', 'blob', - '--min-tls-version', 'TLS1_2') - - # Create container - callaz('storage', 'container', 'create', - '--name', name, - '--account-name', account_name) + try: + # Create storage account + callaz('storage', 'account', 'create', + '--name', account_name, + '--resource-group', rg, + '--encryption-services', 'blob', + '--min-tls-version', 'TLS1_2') + except Exception as e: + if 'ResourceAlreadyExists' not in str(e) and 'already exists' not in str(e).lower(): + raise + + try: + # Create container + callaz('storage', 'container', 'create', + '--name', name, + '--account-name', account_name) + except Exception as e: + if 'ResourceAlreadyExists' not in str(e) and 'already exists' not in str(e).lower(): + raise # Get connection string keys = callaz('storage', 'account', 'show-connection-string', @@ -362,14 +455,20 @@ def bucket(name, provider='docker', **kw): return (env_dict, None) elif provider == 'gcp': + _check_cli('gcloud') location = kw.get('location', 'us') - # Create bucket - subprocess.run(['gcloud', 'storage', 'buckets', 'create', - f'gs://{name}', - '--location', location, - '--uniform-bucket-level-access'], - capture_output=True, text=True, check=True) + try: + # Create bucket + subprocess.run(['gcloud', 'storage', 'buckets', 'create', + f'gs://{name}', + '--location', location, + '--uniform-bucket-level-access', + '--quiet'], + capture_output=True, text=True, check=True) + except subprocess.CalledProcessError as e: + if 'already exists' not in e.stderr.lower(): + raise env_dict = { 'GCS_BUCKET': name, @@ -421,30 +520,39 @@ def llm(name='gpt-4o', provider='openai', **kw): return (env_dict, None) elif provider == 'azure': + _check_cli('az') from .azure import callaz rg = kw.get('resource_group') location = kw.get('location', 'eastus') - # Create Azure OpenAI resource - callaz('cognitiveservices', 'account', 'create', - '--name', name, - '--resource-group', rg, - '--kind', 'OpenAI', - '--sku', 'S0', - '--location', location) - - # Deploy model - deployment_name = kw.get('deployment', f'{name}-deployment') - model_name = kw.get('model_name', 'gpt-4') - callaz('cognitiveservices', 'account', 'deployment', 'create', - '--name', name, - '--resource-group', rg, - '--deployment-name', deployment_name, - '--model-name', model_name, - '--model-version', kw.get('model_version', '0613'), - '--model-format', 'OpenAI', - '--sku-capacity', str(kw.get('capacity', 1)), - '--sku-name', 'Standard') + try: + # Create Azure OpenAI resource + callaz('cognitiveservices', 'account', 'create', + '--name', name, + '--resource-group', rg, + '--kind', 'OpenAI', + '--sku', 'S0', + '--location', location) + except Exception as e: + if 'ResourceAlreadyExists' not in str(e) and 'already exists' not in str(e).lower(): + raise + + try: + # Deploy model + deployment_name = kw.get('deployment', f'{name}-deployment') + model_name = kw.get('model_name', 'gpt-4') + callaz('cognitiveservices', 'account', 'deployment', 'create', + '--name', name, + '--resource-group', rg, + '--deployment-name', deployment_name, + '--model-name', model_name, + '--model-version', kw.get('model_version', '0613'), + '--model-format', 'OpenAI', + '--sku-capacity', str(kw.get('capacity', 1)), + '--sku-name', 'Standard') + except Exception as e: + if 'ResourceAlreadyExists' not in str(e) and 'already exists' not in str(e).lower(): + raise # Get endpoint and key account = callaz('cognitiveservices', 'account', 'show', @@ -482,49 +590,72 @@ def llm(name='gpt-4o', provider='openai', **kw): def function(name, runtime='python3.12', handler='main.handler', provider='aws', **kw): 'Provision serverless function' if provider == 'aws': + _check_cli('aws') from .aws import callaws role = kw.get('role') or os.environ.get('LAMBDA_ROLE_ARN') zip_path = kw.get('zip_path', 'function.zip') timeout = kw.get('timeout', 30) memory = kw.get('memory', 256) - result = callaws('lambda', 'create-function', - '--function-name', name, - '--runtime', runtime, - '--handler', handler, - '--role', role, - '--zip-file', f'fileb://{zip_path}', - '--timeout', str(timeout), - '--memory-size', str(memory)) + try: + result = callaws('lambda', 'create-function', + '--function-name', name, + '--runtime', runtime, + '--handler', handler, + '--role', role, + '--zip-file', f'fileb://{zip_path}', + '--timeout', str(timeout), + '--memory-size', str(memory)) + + env_dict = { + 'FUNCTION_ARN': result['FunctionArn'], + 'FUNCTION_NAME': name, + 'FUNCTION_PROVIDER': 'lambda' + } + except Exception as e: + if 'ResourceConflictException' in str(e): + # Get existing function + result = callaws('lambda', 'get-function', '--function-name', name) + env_dict = { + 'FUNCTION_ARN': result['Configuration']['FunctionArn'], + 'FUNCTION_NAME': name, + 'FUNCTION_PROVIDER': 'lambda' + } + else: + raise - env_dict = { - 'FUNCTION_ARN': result['FunctionArn'], - 'FUNCTION_NAME': name, - 'FUNCTION_PROVIDER': 'lambda' - } return (env_dict, None) elif provider == 'azure': + _check_cli('az') from .azure import callaz rg = kw.get('resource_group') location = kw.get('location', 'eastus') storage_account = kw.get('storage_account', f'{name}storage') - # Create storage account - callaz('storage', 'account', 'create', - '--name', storage_account, - '--resource-group', rg, - '--location', location) - - # Create function app - callaz('functionapp', 'create', - '--name', name, - '--resource-group', rg, - '--consumption-plan-location', location, - '--runtime', 'python', - '--runtime-version', runtime.replace('python', ''), - '--storage-account', storage_account, - '--os-type', 'Linux') + try: + # Create storage account + callaz('storage', 'account', 'create', + '--name', storage_account, + '--resource-group', rg, + '--location', location) + except Exception as e: + if 'ResourceAlreadyExists' not in str(e) and 'already exists' not in str(e).lower(): + raise + + try: + # Create function app + callaz('functionapp', 'create', + '--name', name, + '--resource-group', rg, + '--consumption-plan-location', location, + '--runtime', 'python', + '--runtime-version', runtime.replace('python', ''), + '--storage-account', storage_account, + '--os-type', 'Linux') + except Exception as e: + if 'ResourceAlreadyExists' not in str(e) and 'already exists' not in str(e).lower(): + raise env_dict = { 'FUNCTION_URL': f'https://{name}.azurewebsites.net', @@ -534,23 +665,34 @@ def function(name, runtime='python3.12', handler='main.handler', provider='aws', return (env_dict, None) elif provider == 'gcp': + _check_cli('gcloud') region = kw.get('region', 'us-central1') entry_point = kw.get('entry_point', handler.split('.')[-1]) - result = subprocess.run(['gcloud', 'functions', 'deploy', name, - '--runtime', runtime, - '--trigger-http', - '--allow-unauthenticated', - '--entry-point', entry_point, - '--region', region], - capture_output=True, text=True, check=True) + try: + result = subprocess.run(['gcloud', 'functions', 'deploy', name, + '--runtime', runtime, + '--trigger-http', + '--allow-unauthenticated', + '--entry-point', entry_point, + '--region', region, + '--quiet'], + capture_output=True, text=True, check=True) + except subprocess.CalledProcessError as e: + if 'already exists' not in e.stderr.lower(): + raise + # Get existing function + result = subprocess.run(['gcloud', 'functions', 'describe', name, + '--region', region, + '--format', 'json'], + capture_output=True, text=True, check=True) # Parse output for URL output = result.stdout url = '' for line in output.split('\n'): - if 'url:' in line.lower(): - url = line.split(':', 1)[1].strip() + if 'url:' in line.lower() or 'httpsTrigger' in line: + url = line.split(':', 1)[1].strip() if ':' in line else '' env_dict = { 'FUNCTION_URL': url, @@ -583,24 +725,33 @@ def search(name='search', provider='docker', **kw): return (env_dict, svc) elif provider == 'aws': + _check_cli('aws') from .aws import callaws instance_type = kw.get('instance_type', 't3.small.search') volume_size = kw.get('volume_size', 20) - result = callaws('opensearch', 'create-domain', - '--domain-name', name, - '--engine-version', 'OpenSearch_2.11', - '--cluster-config', json.dumps({ - 'InstanceType': instance_type, - 'InstanceCount': 1 - }), - '--ebs-options', json.dumps({ - 'EBSEnabled': True, - 'VolumeType': 'gp3', - 'VolumeSize': volume_size - })) - - endpoint = result['DomainStatus']['Endpoint'] + try: + result = callaws('opensearch', 'create-domain', + '--domain-name', name, + '--engine-version', 'OpenSearch_2.11', + '--cluster-config', json.dumps({ + 'InstanceType': instance_type, + 'InstanceCount': 1 + }), + '--ebs-options', json.dumps({ + 'EBSEnabled': True, + 'VolumeType': 'gp3', + 'VolumeSize': volume_size + })) + + endpoint = result['DomainStatus']['Endpoint'] + except Exception as e: + if 'ResourceAlreadyExistsException' in str(e): + # Describe existing domain + result = callaws('opensearch', 'describe-domain', '--domain-name', name) + endpoint = result['DomainStatus']['Endpoint'] + else: + raise env_dict = { 'SEARCH_URL': f'https://{endpoint}', @@ -609,15 +760,20 @@ def search(name='search', provider='docker', **kw): return (env_dict, None) elif provider == 'azure': + _check_cli('az') from .azure import callaz rg = kw.get('resource_group') sku = kw.get('sku', 'basic') - # Create search service - callaz('search', 'service', 'create', - '--name', name, - '--resource-group', rg, - '--sku', sku) + try: + # Create search service + callaz('search', 'service', 'create', + '--name', name, + '--resource-group', rg, + '--sku', sku) + except Exception as e: + if 'ResourceAlreadyExists' not in str(e) and 'already exists' not in str(e).lower(): + raise # Get admin key keys = callaz('search', 'admin-key', 'show', diff --git a/fastops/teardown.py b/fastops/teardown.py new file mode 100644 index 0000000..6cfe85d --- /dev/null +++ b/fastops/teardown.py @@ -0,0 +1,433 @@ +"""Resource teardown and lifecycle management. Safely destroy provisioned resources.""" + +__all__ = ['destroy', 'destroy_stack', 'status'] + +import os +import json +import subprocess +import shutil + + +def destroy(resource_type, name, provider='docker', **kw): + 'Destroy a single provisioned resource' + dispatchers = { + 'database': _destroy_database, + 'cache': _destroy_cache, + 'queue': _destroy_queue, + 'bucket': _destroy_bucket, + 'llm': _destroy_llm, + 'search': _destroy_search, + 'function': _destroy_function + } + + if resource_type not in dispatchers: + return {'destroyed': False, 'resource': name, 'provider': provider, + 'message': f'Unknown resource type: {resource_type}'} + + result = dispatchers[resource_type](name, provider, **kw) + result.setdefault('resource', name) + result.setdefault('provider', provider) + return result + + +def _destroy_database(name, provider, **kw): + 'Destroy a database instance' + if provider == 'docker': + return {'destroyed': True, 'message': 'Remove via docker compose down -v'} + + elif provider == 'aws': + from .aws import callaws + try: + callaws('rds', 'delete-db-instance', + '--db-instance-identifier', name, + '--skip-final-snapshot', + '--delete-automated-backups') + except Exception as e: + if 'DBInstanceNotFound' in str(e): + return {'destroyed': False, 'message': f'RDS instance {name} not found'} + raise + return {'destroyed': True, 'message': f'RDS instance {name} deletion initiated'} + + elif provider == 'azure': + from .azure import callaz + rg = kw.get('resource_group') + try: + callaz('postgres', 'flexible-server', 'delete', + '--name', name, + '--resource-group', rg, + '--yes') + except Exception as e: + if 'ResourceNotFound' in str(e): + return {'destroyed': False, 'message': f'Azure DB {name} not found'} + raise + return {'destroyed': True, 'message': f'Azure Postgres {name} deleted'} + + return {'destroyed': False, 'message': f'Unsupported provider: {provider}'} + + +def _destroy_cache(name, provider, **kw): + 'Destroy a cache instance' + if provider == 'docker': + return {'destroyed': True, 'message': 'Remove via docker compose down -v'} + + elif provider == 'aws': + from .aws import callaws + try: + callaws('elasticache', 'delete-cache-cluster', + '--cache-cluster-id', name) + except Exception as e: + if 'CacheClusterNotFound' in str(e): + return {'destroyed': False, 'message': f'ElastiCache cluster {name} not found'} + raise + return {'destroyed': True, 'message': f'ElastiCache cluster {name} deletion initiated'} + + elif provider == 'azure': + from .azure import callaz + rg = kw.get('resource_group') + try: + callaz('redis', 'delete', + '--name', name, + '--resource-group', rg, + '--yes') + except Exception as e: + if 'ResourceNotFound' in str(e): + return {'destroyed': False, 'message': f'Azure Redis {name} not found'} + raise + return {'destroyed': True, 'message': f'Azure Redis {name} deleted'} + + return {'destroyed': False, 'message': f'Unsupported provider: {provider}'} + + +def _destroy_queue(name, provider, **kw): + 'Destroy a message queue' + if provider == 'docker': + return {'destroyed': True, 'message': 'Remove via docker compose down -v'} + + elif provider == 'aws': + from .aws import callaws + try: + # Get queue URL first + result = callaws('sqs', 'get-queue-url', '--queue-name', name) + url = result['QueueUrl'] + # Delete the queue + callaws('sqs', 'delete-queue', '--queue-url', url) + except Exception as e: + if 'NonExistentQueue' in str(e) or 'QueueDoesNotExist' in str(e): + return {'destroyed': False, 'message': f'SQS queue {name} not found'} + raise + return {'destroyed': True, 'message': f'SQS queue {name} deleted'} + + elif provider == 'azure': + from .azure import callaz + rg = kw.get('resource_group') + namespace = kw.get('namespace', f'{name}-ns') + try: + callaz('servicebus', 'namespace', 'delete', + '--name', namespace, + '--resource-group', rg, + '--yes') + except Exception as e: + if 'ResourceNotFound' in str(e): + return {'destroyed': False, 'message': f'Azure ServiceBus namespace {namespace} not found'} + raise + return {'destroyed': True, 'message': f'Azure ServiceBus namespace {namespace} deleted'} + + elif provider == 'gcp': + try: + # Delete subscription first + sub_name = f'{name}-sub' + subprocess.run(['gcloud', 'pubsub', 'subscriptions', 'delete', sub_name, '--quiet'], + capture_output=True, text=True, check=True) + # Delete topic + subprocess.run(['gcloud', 'pubsub', 'topics', 'delete', name, '--quiet'], + capture_output=True, text=True, check=True) + except subprocess.CalledProcessError as e: + if 'NOT_FOUND' in e.stderr or 'does not exist' in e.stderr.lower(): + return {'destroyed': False, 'message': f'GCP Pub/Sub topic {name} not found'} + raise + return {'destroyed': True, 'message': f'GCP Pub/Sub topic {name} deleted'} + + return {'destroyed': False, 'message': f'Unsupported provider: {provider}'} + + +def _destroy_bucket(name, provider, **kw): + 'Destroy object storage bucket' + if provider == 'docker': + return {'destroyed': True, 'message': 'Remove via docker compose down -v'} + + elif provider == 'aws': + from .aws import callaws + try: + # Empty bucket first + callaws('s3', 'rm', f's3://{name}', '--recursive') + # Delete bucket + callaws('s3api', 'delete-bucket', '--bucket', name) + except Exception as e: + if 'NoSuchBucket' in str(e): + return {'destroyed': False, 'message': f'S3 bucket {name} not found'} + raise + return {'destroyed': True, 'message': f'S3 bucket {name} deleted'} + + elif provider == 'azure': + from .azure import callaz + rg = kw.get('resource_group') + account_name = kw.get('account_name', name.replace('-', '').replace('_', '')[:24]) + try: + # Delete container + callaz('storage', 'container', 'delete', + '--name', name, + '--account-name', account_name, + '--yes') + # Delete storage account + callaz('storage', 'account', 'delete', + '--name', account_name, + '--resource-group', rg, + '--yes') + except Exception as e: + if 'ResourceNotFound' in str(e) or 'NotFound' in str(e): + return {'destroyed': False, 'message': f'Azure storage {name} not found'} + raise + return {'destroyed': True, 'message': f'Azure storage account {account_name} deleted'} + + elif provider == 'gcp': + try: + subprocess.run(['gcloud', 'storage', 'rm', '-r', f'gs://{name}', '--quiet'], + capture_output=True, text=True, check=True) + except subprocess.CalledProcessError as e: + if 'NOT_FOUND' in e.stderr or 'does not exist' in e.stderr.lower(): + return {'destroyed': False, 'message': f'GCS bucket {name} not found'} + raise + return {'destroyed': True, 'message': f'GCS bucket {name} deleted'} + + return {'destroyed': False, 'message': f'Unsupported provider: {provider}'} + + +def _destroy_llm(name, provider, **kw): + 'Destroy LLM endpoint' + if provider == 'docker': + return {'destroyed': True, 'message': 'Remove via docker compose down -v'} + + elif provider == 'openai': + return {'destroyed': True, 'message': 'No teardown needed for OpenAI'} + + elif provider == 'azure': + from .azure import callaz + rg = kw.get('resource_group') + deployment_name = kw.get('deployment', f'{name}-deployment') + try: + # Delete deployment + callaz('cognitiveservices', 'account', 'deployment', 'delete', + '--name', name, + '--resource-group', rg, + '--deployment-name', deployment_name) + # Delete account + callaz('cognitiveservices', 'account', 'delete', + '--name', name, + '--resource-group', rg) + except Exception as e: + if 'ResourceNotFound' in str(e): + return {'destroyed': False, 'message': f'Azure OpenAI {name} not found'} + raise + return {'destroyed': True, 'message': f'Azure OpenAI account {name} deleted'} + + elif provider == 'aws': + return {'destroyed': True, 'message': 'No teardown needed for Bedrock'} + + return {'destroyed': False, 'message': f'Unsupported provider: {provider}'} + + +def _destroy_search(name, provider, **kw): + 'Destroy search engine' + if provider == 'docker': + return {'destroyed': True, 'message': 'Remove via docker compose down -v'} + + elif provider == 'aws': + from .aws import callaws + try: + callaws('opensearch', 'delete-domain', '--domain-name', name) + except Exception as e: + if 'ResourceNotFoundException' in str(e): + return {'destroyed': False, 'message': f'OpenSearch domain {name} not found'} + raise + return {'destroyed': True, 'message': f'OpenSearch domain {name} deletion initiated'} + + elif provider == 'azure': + from .azure import callaz + rg = kw.get('resource_group') + try: + callaz('search', 'service', 'delete', + '--name', name, + '--resource-group', rg, + '--yes') + except Exception as e: + if 'ResourceNotFound' in str(e): + return {'destroyed': False, 'message': f'Azure Search {name} not found'} + raise + return {'destroyed': True, 'message': f'Azure Search {name} deleted'} + + return {'destroyed': False, 'message': f'Unsupported provider: {provider}'} + + +def _destroy_function(name, provider, **kw): + 'Destroy serverless function' + if provider == 'aws': + from .aws import callaws + try: + callaws('lambda', 'delete-function', '--function-name', name) + except Exception as e: + if 'ResourceNotFoundException' in str(e): + return {'destroyed': False, 'message': f'Lambda function {name} not found'} + raise + return {'destroyed': True, 'message': f'Lambda function {name} deleted'} + + elif provider == 'azure': + from .azure import callaz + rg = kw.get('resource_group') + try: + callaz('functionapp', 'delete', + '--name', name, + '--resource-group', rg, + '--yes') + except Exception as e: + if 'ResourceNotFound' in str(e): + return {'destroyed': False, 'message': f'Azure Function {name} not found'} + raise + return {'destroyed': True, 'message': f'Azure Function {name} deleted'} + + elif provider == 'gcp': + region = kw.get('region', 'us-central1') + try: + subprocess.run(['gcloud', 'functions', 'delete', name, + '--quiet', '--region', region], + capture_output=True, text=True, check=True) + except subprocess.CalledProcessError as e: + if 'NOT_FOUND' in e.stderr or 'does not exist' in e.stderr.lower(): + return {'destroyed': False, 'message': f'GCP function {name} not found'} + raise + return {'destroyed': True, 'message': f'GCP function {name} deleted'} + + return {'destroyed': False, 'message': f'Unsupported provider: {provider}'} + + +def _infer_resource_type(env_dict): + 'Infer resource type from environment variables' + if 'DATABASE_URL' in env_dict: + return 'database' + elif 'REDIS_URL' in env_dict: + return 'cache' + elif 'QUEUE_URL' in env_dict or 'QUEUE_TOPIC' in env_dict: + return 'queue' + elif any(k in env_dict for k in ['S3_ENDPOINT', 'S3_BUCKET', 'AZURE_STORAGE_CONNECTION_STRING', 'GCS_BUCKET']): + return 'bucket' + elif 'LLM_ENDPOINT' in env_dict or 'LLM_MODEL' in env_dict: + return 'llm' + elif 'SEARCH_URL' in env_dict: + return 'search' + elif 'FUNCTION_ARN' in env_dict or 'FUNCTION_URL' in env_dict: + return 'function' + else: + return 'unknown' + + +def destroy_stack(resources, provider='docker', **kw): + 'Tear down all resources in a stack (reverse order for dependency safety)' + results = {} + # Reverse to tear down dependents before dependencies + for name in reversed(list(resources.keys())): + resource_fn = resources[name] + # Infer resource type from the function name or env output + env, _ = resource_fn() + rtype = _infer_resource_type(env) + results[name] = destroy(rtype, name, provider, **kw) + return results + + +def status(resource_type, name, provider='docker', **kw): + 'Quick health check for a provisioned resource' + if provider == 'docker': + # Check if container is running + result = subprocess.run(['docker', 'inspect', '--format', '{{.State.Status}}', name], + capture_output=True, text=True) + running = result.returncode == 0 and 'running' in result.stdout + return {'healthy': running, 'provider': 'docker', 'name': name} + + elif provider == 'aws': + from .aws import callaws + try: + if resource_type == 'database': + result = callaws('rds', 'describe-db-instances', + '--db-instance-identifier', name) + status_val = result['DBInstances'][0]['DBInstanceStatus'] + return {'healthy': status_val == 'available', 'provider': 'aws', + 'name': name, 'status': status_val} + + elif resource_type == 'cache': + result = callaws('elasticache', 'describe-cache-clusters', + '--cache-cluster-id', name) + status_val = result['CacheClusters'][0]['CacheClusterStatus'] + return {'healthy': status_val == 'available', 'provider': 'aws', + 'name': name, 'status': status_val} + + elif resource_type == 'bucket': + # Try to list bucket (will fail if not exists) + callaws('s3api', 'head-bucket', '--bucket', name) + return {'healthy': True, 'provider': 'aws', 'name': name} + + elif resource_type == 'search': + result = callaws('opensearch', 'describe-domain', '--domain-name', name) + status_val = result['DomainStatus']['Processing'] + return {'healthy': not status_val, 'provider': 'aws', + 'name': name, 'processing': status_val} + + elif resource_type == 'function': + result = callaws('lambda', 'get-function', '--function-name', name) + state = result['Configuration']['State'] + return {'healthy': state == 'Active', 'provider': 'aws', + 'name': name, 'state': state} + + except Exception as e: + return {'healthy': False, 'provider': 'aws', 'name': name, + 'message': f'Resource not found or error: {str(e)}'} + + elif provider == 'azure': + from .azure import callaz + rg = kw.get('resource_group') + try: + if resource_type == 'database': + result = callaz('postgres', 'flexible-server', 'show', + '--name', name, + '--resource-group', rg) + state = result.get('state', '') + return {'healthy': state == 'Ready', 'provider': 'azure', + 'name': name, 'state': state} + + elif resource_type == 'cache': + result = callaz('redis', 'show', + '--name', name, + '--resource-group', rg) + status_val = result.get('provisioningState', '') + return {'healthy': status_val == 'Succeeded', 'provider': 'azure', + 'name': name, 'status': status_val} + + elif resource_type == 'search': + result = callaz('search', 'service', 'show', + '--name', name, + '--resource-group', rg) + status_val = result.get('provisioningState', '') + return {'healthy': status_val == 'Succeeded', 'provider': 'azure', + 'name': name, 'status': status_val} + + elif resource_type == 'function': + result = callaz('functionapp', 'show', + '--name', name, + '--resource-group', rg) + state = result.get('state', '') + return {'healthy': state == 'Running', 'provider': 'azure', + 'name': name, 'state': state} + + except Exception as e: + return {'healthy': False, 'provider': 'azure', 'name': name, + 'message': f'Resource not found or error: {str(e)}'} + + return {'healthy': False, 'provider': provider, 'name': name, + 'message': 'Unsupported provider or resource type'} From baeaa0d95b6e5b3d97cfc5459402b53984572753 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 04:35:03 +0000 Subject: [PATCH 3/3] Optimize teardown.py to be under 400 lines Co-authored-by: Karthik777 <7102951+Karthik777@users.noreply.github.com> --- fastops/teardown.py | 231 +++++++++++++++----------------------------- 1 file changed, 80 insertions(+), 151 deletions(-) diff --git a/fastops/teardown.py b/fastops/teardown.py index 6cfe85d..17da8bc 100644 --- a/fastops/teardown.py +++ b/fastops/teardown.py @@ -8,22 +8,29 @@ import shutil +def _docker_noop(): + 'Docker resources managed by compose' + return {'destroyed': True, 'message': 'Remove via docker compose down -v'} + +def _not_found(msg): + 'Resource not found response' + return {'destroyed': False, 'message': msg} + +def _success(msg): + 'Successful deletion response' + return {'destroyed': True, 'message': msg} + + def destroy(resource_type, name, provider='docker', **kw): 'Destroy a single provisioned resource' dispatchers = { - 'database': _destroy_database, - 'cache': _destroy_cache, - 'queue': _destroy_queue, - 'bucket': _destroy_bucket, - 'llm': _destroy_llm, - 'search': _destroy_search, + 'database': _destroy_database, 'cache': _destroy_cache, 'queue': _destroy_queue, + 'bucket': _destroy_bucket, 'llm': _destroy_llm, 'search': _destroy_search, 'function': _destroy_function } - if resource_type not in dispatchers: return {'destroyed': False, 'resource': name, 'provider': provider, 'message': f'Unknown resource type: {resource_type}'} - result = dispatchers[resource_type](name, provider, **kw) result.setdefault('resource', name) result.setdefault('provider', provider) @@ -32,240 +39,171 @@ def destroy(resource_type, name, provider='docker', **kw): def _destroy_database(name, provider, **kw): 'Destroy a database instance' - if provider == 'docker': - return {'destroyed': True, 'message': 'Remove via docker compose down -v'} - + if provider == 'docker': return _docker_noop() elif provider == 'aws': from .aws import callaws try: - callaws('rds', 'delete-db-instance', - '--db-instance-identifier', name, - '--skip-final-snapshot', - '--delete-automated-backups') + callaws('rds', 'delete-db-instance', '--db-instance-identifier', name, + '--skip-final-snapshot', '--delete-automated-backups') except Exception as e: - if 'DBInstanceNotFound' in str(e): - return {'destroyed': False, 'message': f'RDS instance {name} not found'} + if 'DBInstanceNotFound' in str(e): return _not_found(f'RDS instance {name} not found') raise - return {'destroyed': True, 'message': f'RDS instance {name} deletion initiated'} - + return _success(f'RDS instance {name} deletion initiated') elif provider == 'azure': from .azure import callaz rg = kw.get('resource_group') try: - callaz('postgres', 'flexible-server', 'delete', - '--name', name, - '--resource-group', rg, - '--yes') + callaz('postgres', 'flexible-server', 'delete', '--name', name, + '--resource-group', rg, '--yes') except Exception as e: - if 'ResourceNotFound' in str(e): - return {'destroyed': False, 'message': f'Azure DB {name} not found'} + if 'ResourceNotFound' in str(e): return _not_found(f'Azure DB {name} not found') raise - return {'destroyed': True, 'message': f'Azure Postgres {name} deleted'} - - return {'destroyed': False, 'message': f'Unsupported provider: {provider}'} + return _success(f'Azure Postgres {name} deleted') + return _not_found(f'Unsupported provider: {provider}') def _destroy_cache(name, provider, **kw): 'Destroy a cache instance' - if provider == 'docker': - return {'destroyed': True, 'message': 'Remove via docker compose down -v'} - + if provider == 'docker': return _docker_noop() elif provider == 'aws': from .aws import callaws try: - callaws('elasticache', 'delete-cache-cluster', - '--cache-cluster-id', name) + callaws('elasticache', 'delete-cache-cluster', '--cache-cluster-id', name) except Exception as e: - if 'CacheClusterNotFound' in str(e): - return {'destroyed': False, 'message': f'ElastiCache cluster {name} not found'} + if 'CacheClusterNotFound' in str(e): return _not_found(f'ElastiCache cluster {name} not found') raise - return {'destroyed': True, 'message': f'ElastiCache cluster {name} deletion initiated'} - + return _success(f'ElastiCache cluster {name} deletion initiated') elif provider == 'azure': from .azure import callaz rg = kw.get('resource_group') try: - callaz('redis', 'delete', - '--name', name, - '--resource-group', rg, - '--yes') + callaz('redis', 'delete', '--name', name, '--resource-group', rg, '--yes') except Exception as e: - if 'ResourceNotFound' in str(e): - return {'destroyed': False, 'message': f'Azure Redis {name} not found'} + if 'ResourceNotFound' in str(e): return _not_found(f'Azure Redis {name} not found') raise - return {'destroyed': True, 'message': f'Azure Redis {name} deleted'} - - return {'destroyed': False, 'message': f'Unsupported provider: {provider}'} + return _success(f'Azure Redis {name} deleted') + return _not_found(f'Unsupported provider: {provider}') def _destroy_queue(name, provider, **kw): 'Destroy a message queue' - if provider == 'docker': - return {'destroyed': True, 'message': 'Remove via docker compose down -v'} - + if provider == 'docker': return _docker_noop() elif provider == 'aws': from .aws import callaws try: - # Get queue URL first result = callaws('sqs', 'get-queue-url', '--queue-name', name) - url = result['QueueUrl'] - # Delete the queue - callaws('sqs', 'delete-queue', '--queue-url', url) + callaws('sqs', 'delete-queue', '--queue-url', result['QueueUrl']) except Exception as e: if 'NonExistentQueue' in str(e) or 'QueueDoesNotExist' in str(e): - return {'destroyed': False, 'message': f'SQS queue {name} not found'} + return _not_found(f'SQS queue {name} not found') raise - return {'destroyed': True, 'message': f'SQS queue {name} deleted'} - + return _success(f'SQS queue {name} deleted') elif provider == 'azure': from .azure import callaz rg = kw.get('resource_group') namespace = kw.get('namespace', f'{name}-ns') try: - callaz('servicebus', 'namespace', 'delete', - '--name', namespace, - '--resource-group', rg, - '--yes') + callaz('servicebus', 'namespace', 'delete', '--name', namespace, + '--resource-group', rg, '--yes') except Exception as e: - if 'ResourceNotFound' in str(e): - return {'destroyed': False, 'message': f'Azure ServiceBus namespace {namespace} not found'} + if 'ResourceNotFound' in str(e): return _not_found(f'Azure ServiceBus namespace {namespace} not found') raise - return {'destroyed': True, 'message': f'Azure ServiceBus namespace {namespace} deleted'} - + return _success(f'Azure ServiceBus namespace {namespace} deleted') elif provider == 'gcp': try: - # Delete subscription first sub_name = f'{name}-sub' subprocess.run(['gcloud', 'pubsub', 'subscriptions', 'delete', sub_name, '--quiet'], capture_output=True, text=True, check=True) - # Delete topic subprocess.run(['gcloud', 'pubsub', 'topics', 'delete', name, '--quiet'], capture_output=True, text=True, check=True) except subprocess.CalledProcessError as e: if 'NOT_FOUND' in e.stderr or 'does not exist' in e.stderr.lower(): - return {'destroyed': False, 'message': f'GCP Pub/Sub topic {name} not found'} + return _not_found(f'GCP Pub/Sub topic {name} not found') raise - return {'destroyed': True, 'message': f'GCP Pub/Sub topic {name} deleted'} - - return {'destroyed': False, 'message': f'Unsupported provider: {provider}'} + return _success(f'GCP Pub/Sub topic {name} deleted') + return _not_found(f'Unsupported provider: {provider}') def _destroy_bucket(name, provider, **kw): 'Destroy object storage bucket' - if provider == 'docker': - return {'destroyed': True, 'message': 'Remove via docker compose down -v'} - + if provider == 'docker': return _docker_noop() elif provider == 'aws': from .aws import callaws try: - # Empty bucket first callaws('s3', 'rm', f's3://{name}', '--recursive') - # Delete bucket callaws('s3api', 'delete-bucket', '--bucket', name) except Exception as e: - if 'NoSuchBucket' in str(e): - return {'destroyed': False, 'message': f'S3 bucket {name} not found'} + if 'NoSuchBucket' in str(e): return _not_found(f'S3 bucket {name} not found') raise - return {'destroyed': True, 'message': f'S3 bucket {name} deleted'} - + return _success(f'S3 bucket {name} deleted') elif provider == 'azure': from .azure import callaz rg = kw.get('resource_group') account_name = kw.get('account_name', name.replace('-', '').replace('_', '')[:24]) try: - # Delete container - callaz('storage', 'container', 'delete', - '--name', name, - '--account-name', account_name, - '--yes') - # Delete storage account - callaz('storage', 'account', 'delete', - '--name', account_name, - '--resource-group', rg, - '--yes') + callaz('storage', 'container', 'delete', '--name', name, + '--account-name', account_name, '--yes') + callaz('storage', 'account', 'delete', '--name', account_name, + '--resource-group', rg, '--yes') except Exception as e: if 'ResourceNotFound' in str(e) or 'NotFound' in str(e): - return {'destroyed': False, 'message': f'Azure storage {name} not found'} + return _not_found(f'Azure storage {name} not found') raise - return {'destroyed': True, 'message': f'Azure storage account {account_name} deleted'} - + return _success(f'Azure storage account {account_name} deleted') elif provider == 'gcp': try: subprocess.run(['gcloud', 'storage', 'rm', '-r', f'gs://{name}', '--quiet'], capture_output=True, text=True, check=True) except subprocess.CalledProcessError as e: if 'NOT_FOUND' in e.stderr or 'does not exist' in e.stderr.lower(): - return {'destroyed': False, 'message': f'GCS bucket {name} not found'} + return _not_found(f'GCS bucket {name} not found') raise - return {'destroyed': True, 'message': f'GCS bucket {name} deleted'} - - return {'destroyed': False, 'message': f'Unsupported provider: {provider}'} + return _success(f'GCS bucket {name} deleted') + return _not_found(f'Unsupported provider: {provider}') def _destroy_llm(name, provider, **kw): 'Destroy LLM endpoint' - if provider == 'docker': - return {'destroyed': True, 'message': 'Remove via docker compose down -v'} - - elif provider == 'openai': - return {'destroyed': True, 'message': 'No teardown needed for OpenAI'} - + if provider == 'docker': return _docker_noop() + elif provider == 'openai': return _success('No teardown needed for OpenAI') elif provider == 'azure': from .azure import callaz rg = kw.get('resource_group') deployment_name = kw.get('deployment', f'{name}-deployment') try: - # Delete deployment callaz('cognitiveservices', 'account', 'deployment', 'delete', - '--name', name, - '--resource-group', rg, - '--deployment-name', deployment_name) - # Delete account + '--name', name, '--resource-group', rg, '--deployment-name', deployment_name) callaz('cognitiveservices', 'account', 'delete', - '--name', name, - '--resource-group', rg) + '--name', name, '--resource-group', rg) except Exception as e: - if 'ResourceNotFound' in str(e): - return {'destroyed': False, 'message': f'Azure OpenAI {name} not found'} + if 'ResourceNotFound' in str(e): return _not_found(f'Azure OpenAI {name} not found') raise - return {'destroyed': True, 'message': f'Azure OpenAI account {name} deleted'} - - elif provider == 'aws': - return {'destroyed': True, 'message': 'No teardown needed for Bedrock'} - - return {'destroyed': False, 'message': f'Unsupported provider: {provider}'} + return _success(f'Azure OpenAI account {name} deleted') + elif provider == 'aws': return _success('No teardown needed for Bedrock') + return _not_found(f'Unsupported provider: {provider}') def _destroy_search(name, provider, **kw): 'Destroy search engine' - if provider == 'docker': - return {'destroyed': True, 'message': 'Remove via docker compose down -v'} - + if provider == 'docker': return _docker_noop() elif provider == 'aws': from .aws import callaws try: callaws('opensearch', 'delete-domain', '--domain-name', name) except Exception as e: - if 'ResourceNotFoundException' in str(e): - return {'destroyed': False, 'message': f'OpenSearch domain {name} not found'} + if 'ResourceNotFoundException' in str(e): return _not_found(f'OpenSearch domain {name} not found') raise - return {'destroyed': True, 'message': f'OpenSearch domain {name} deletion initiated'} - + return _success(f'OpenSearch domain {name} deletion initiated') elif provider == 'azure': from .azure import callaz rg = kw.get('resource_group') try: - callaz('search', 'service', 'delete', - '--name', name, - '--resource-group', rg, - '--yes') + callaz('search', 'service', 'delete', '--name', name, '--resource-group', rg, '--yes') except Exception as e: - if 'ResourceNotFound' in str(e): - return {'destroyed': False, 'message': f'Azure Search {name} not found'} + if 'ResourceNotFound' in str(e): return _not_found(f'Azure Search {name} not found') raise - return {'destroyed': True, 'message': f'Azure Search {name} deleted'} - - return {'destroyed': False, 'message': f'Unsupported provider: {provider}'} + return _success(f'Azure Search {name} deleted') + return _not_found(f'Unsupported provider: {provider}') def _destroy_function(name, provider, **kw): @@ -275,38 +213,29 @@ def _destroy_function(name, provider, **kw): try: callaws('lambda', 'delete-function', '--function-name', name) except Exception as e: - if 'ResourceNotFoundException' in str(e): - return {'destroyed': False, 'message': f'Lambda function {name} not found'} + if 'ResourceNotFoundException' in str(e): return _not_found(f'Lambda function {name} not found') raise - return {'destroyed': True, 'message': f'Lambda function {name} deleted'} - + return _success(f'Lambda function {name} deleted') elif provider == 'azure': from .azure import callaz rg = kw.get('resource_group') try: - callaz('functionapp', 'delete', - '--name', name, - '--resource-group', rg, - '--yes') + callaz('functionapp', 'delete', '--name', name, '--resource-group', rg, '--yes') except Exception as e: - if 'ResourceNotFound' in str(e): - return {'destroyed': False, 'message': f'Azure Function {name} not found'} + if 'ResourceNotFound' in str(e): return _not_found(f'Azure Function {name} not found') raise - return {'destroyed': True, 'message': f'Azure Function {name} deleted'} - + return _success(f'Azure Function {name} deleted') elif provider == 'gcp': region = kw.get('region', 'us-central1') try: - subprocess.run(['gcloud', 'functions', 'delete', name, - '--quiet', '--region', region], + subprocess.run(['gcloud', 'functions', 'delete', name, '--quiet', '--region', region], capture_output=True, text=True, check=True) except subprocess.CalledProcessError as e: if 'NOT_FOUND' in e.stderr or 'does not exist' in e.stderr.lower(): - return {'destroyed': False, 'message': f'GCP function {name} not found'} + return _not_found(f'GCP function {name} not found') raise - return {'destroyed': True, 'message': f'GCP function {name} deleted'} - - return {'destroyed': False, 'message': f'Unsupported provider: {provider}'} + return _success(f'GCP function {name} deleted') + return _not_found(f'Unsupported provider: {provider}') def _infer_resource_type(env_dict):