feat: add AWS Secrets Manager Integration

This commit is contained in:
tomoya-kawaguchi
2025-05-29 12:42:05 +09:00
parent a17100e512
commit 1955157e9a
13 changed files with 608 additions and 3 deletions

View File

@ -81,3 +81,65 @@ def _find_pyproject() -> Optional[Path]:
pyproject_path = _find_pyproject()
if pyproject_path is not None:
get_settings().load_file(pyproject_path, env=f'tool.{PR_AGENT_TOML_KEY}')
def apply_secrets_manager_config():
"""
Retrieve configuration from AWS Secrets Manager and override existing settings
"""
try:
from pr_agent.secret_providers import get_secret_provider
from pr_agent.log import get_logger
secret_provider = get_secret_provider()
if not secret_provider:
return
# Execute only when AWS Secrets Manager specific method is available
if (hasattr(secret_provider, 'get_all_secrets') and
get_settings().get("CONFIG.SECRET_PROVIDER") == 'aws_secrets_manager'):
try:
secrets = secret_provider.get_all_secrets()
if secrets:
apply_secrets_to_config(secrets)
get_logger().info("Applied AWS Secrets Manager configuration")
except Exception as e:
get_logger().error(f"Failed to apply AWS Secrets Manager config: {e}")
except Exception as e:
# Fail silently when secret provider is not configured
try:
from pr_agent.log import get_logger
get_logger().debug(f"Secret provider not configured: {e}")
except:
# Fail completely silently if log module is not available
pass
def apply_secrets_to_config(secrets: dict):
"""
Apply secret dictionary to configuration
Configuration override with same pattern as Google Cloud Storage
"""
try:
from pr_agent.log import get_logger
except:
# Do nothing if logging is not available
def get_logger():
class DummyLogger:
def debug(self, msg): pass
return DummyLogger()
for key, value in secrets.items():
if '.' in key: # nested key like "openai.key"
parts = key.split('.')
if len(parts) == 2:
section, setting = parts
# Convert case to match Dynaconf pattern
section_upper = section.upper()
setting_upper = setting.upper()
# Set only when no existing value (prioritize environment variables)
current_value = get_settings().get(f"{section_upper}.{setting_upper}")
if current_value is None or current_value == "":
get_settings().set(f"{section_upper}.{setting_upper}", value)
get_logger().debug(f"Set {section}.{setting} from AWS Secrets Manager")

View File

@ -13,5 +13,12 @@ def get_secret_provider():
return GoogleCloudStorageSecretProvider()
except Exception as e:
raise ValueError(f"Failed to initialize google_cloud_storage secret provider {provider_id}") from e
elif provider_id == 'aws_secrets_manager':
try:
from pr_agent.secret_providers.aws_secrets_manager_provider import \
AWSSecretsManagerProvider
return AWSSecretsManagerProvider()
except Exception as e:
raise ValueError(f"Failed to initialize aws_secrets_manager secret provider {provider_id}") from e
else:
raise ValueError("Unknown SECRET_PROVIDER")

View File

@ -0,0 +1,79 @@
import json
import boto3
from botocore.exceptions import ClientError
from pr_agent.config_loader import get_settings
from pr_agent.log import get_logger
from pr_agent.secret_providers.secret_provider import SecretProvider
class AWSSecretsManagerProvider(SecretProvider):
def __init__(self):
try:
# AWS credentials are automatically retrieved from environment variables or IAM roles
# Region configuration is flexible like Google Cloud Storage pattern
region_name = get_settings().get("aws_secrets_manager.region_name") or \
get_settings().get("aws.AWS_REGION_NAME")
if region_name:
self.client = boto3.client('secretsmanager', region_name=region_name)
else:
self.client = boto3.client('secretsmanager')
# Require secret_arn similar to Google Cloud Storage pattern
self.secret_arn = get_settings().aws_secrets_manager.secret_arn
except Exception as e:
get_logger().error(f"Failed to initialize AWS Secrets Manager Provider: {e}")
raise e
def get_secret(self, secret_name: str) -> str:
"""
Retrieve individual secret by name (for webhook tokens)
Same error handling pattern as Google Cloud Storage
"""
try:
response = self.client.get_secret_value(SecretId=secret_name)
return response['SecretString']
except Exception as e:
get_logger().warning(f"Failed to get secret {secret_name} from AWS Secrets Manager: {e}")
return ""
def get_all_secrets(self) -> dict:
"""
Retrieve all secrets for configuration override
AWS Secrets Manager specific method (not available in Google Cloud Storage)
"""
try:
response = self.client.get_secret_value(SecretId=self.secret_arn)
return json.loads(response['SecretString'])
except Exception as e:
get_logger().error(f"Failed to get secrets from AWS Secrets Manager {self.secret_arn}: {e}")
return {}
def store_secret(self, secret_name: str, secret_value: str):
"""
Same error handling pattern as Google Cloud Storage
"""
try:
# Update existing secret
self.client.update_secret(
SecretId=secret_name,
SecretString=secret_value
)
except ClientError as e:
if e.response['Error']['Code'] == 'ResourceNotFoundException':
# Create new secret if it doesn't exist
try:
self.client.create_secret(
Name=secret_name,
SecretString=secret_value
)
except Exception as create_error:
get_logger().error(f"Failed to store secret {secret_name} in AWS Secrets Manager: {create_error}")
raise create_error
else:
get_logger().error(f"Failed to store secret {secret_name} in AWS Secrets Manager: {e}")
raise e
except Exception as e:
get_logger().error(f"Failed to store secret {secret_name} in AWS Secrets Manager: {e}")
raise e

View File

@ -5,6 +5,20 @@ from starlette_context.middleware import RawContextMiddleware
from pr_agent.servers.github_app import router
# Execute AWS Secrets Manager configuration override at module load time
# Initialize with same pattern as Google Cloud Storage provider
try:
from pr_agent.config_loader import apply_secrets_manager_config
apply_secrets_manager_config()
except Exception as e:
# Handle initialization failure silently (fallback to environment variables)
try:
from pr_agent.log import get_logger
get_logger().debug(f"AWS Secrets Manager initialization failed, falling back to environment variables: {e}")
except:
# Fail completely silently if log module is not available
pass
middleware = [Middleware(RawContextMiddleware)]
app = FastAPI(middleware=middleware)
app.include_router(router)

View File

@ -121,4 +121,18 @@ api_base = ""
[aws]
AWS_ACCESS_KEY_ID = ""
AWS_SECRET_ACCESS_KEY = ""
AWS_REGION_NAME = ""
AWS_REGION_NAME = ""
# AWS Secrets Manager (for secure secret management in Lambda environments)
[aws_secrets_manager]
secret_arn = "" # The ARN of the AWS Secrets Manager secret containing PR-Agent configuration
region_name = "" # Optional: specific AWS region (defaults to AWS_REGION_NAME or Lambda region)
# AWS Secrets Manager secret should contain JSON with configuration overrides:
# Example secret value:
# {
# "openai.key": "sk-...",
# "github.webhook_secret": "your-webhook-secret",
# "github.user_token": "ghp_...",
# "gitlab.personal_access_token": "glpat-..."
# }

View File

@ -39,7 +39,7 @@ allow_dynamic_context=true
max_extra_lines_before_dynamic_context = 10 # will try to include up to 10 extra lines before the hunk in the patch, until we reach an enclosing function or class
patch_extra_lines_before = 5 # Number of extra lines (+3 default ones) to include before each hunk in the patch
patch_extra_lines_after = 1 # Number of extra lines (+3 default ones) to include after each hunk in the patch
secret_provider=""
secret_provider="" # "" (disabled), "google_cloud_storage", or "aws_secrets_manager" for secure secret management
cli_mode=false
ai_disclaimer_title="" # Pro feature, title for a collapsible disclaimer to AI outputs
ai_disclaimer="" # Pro feature, full text for the AI disclaimer