diff --git a/__main__.py b/__main__.py index 3cf1706..a695345 100644 --- a/__main__.py +++ b/__main__.py @@ -1,4 +1,4 @@ -# ../konductor/__main__.py +# ./__main__.py """ Konductor Infrastructure as Code Platform @@ -8,53 +8,41 @@ import sys from pulumi import log, export import pulumi +from typing import NoReturn from modules.core.initialization import initialize_pulumi from modules.core.config import get_enabled_modules, get_stack_outputs from modules.core.metadata import setup_global_metadata from modules.core.deployment import DeploymentManager -def main() -> None: +def main() -> NoReturn: """ Main entry point for Konductor's Pulumi Python Infrastructure as Code (IaC). + + Raises: + SystemExit: With code 1 on error, implicit 0 on success """ try: - # Initialize Pulumi and load configuration + # Initialize init_config = initialize_pulumi() - - # Set up global metadata setup_global_metadata(init_config) - # Get the list of enabled modules from the configuration + # Get enabled modules modules_to_deploy = get_enabled_modules(init_config.config) - if not modules_to_deploy: - log.info("No modules to deploy.") - else: - log.info(f"Deploying modules: {', '.join(modules_to_deploy)}") - - # Create a DeploymentManager with the initialized configuration - deployment_manager = DeploymentManager(init_config) - - # Deploy the enabled modules - deployment_manager.deploy_modules(modules_to_deploy) - - # Generate and export stack outputs - try: - stack_outputs = get_stack_outputs(init_config) + log.info("No modules to deploy") + return - # Export each section separately for better organization - export("compliance", stack_outputs["compliance"]) - export("config", stack_outputs["config"]) - export("k8s_app_versions", stack_outputs["k8s_app_versions"]) + # Deploy + deployment_manager = DeploymentManager(init_config) + deployment_manager.deploy_modules(modules_to_deploy) - log.info("Successfully exported stack outputs") - except Exception as e: - log.error(f"Failed to export stack outputs: {str(e)}") - raise + # Export results + stack_outputs = get_stack_outputs(init_config) + export("outputs", stack_outputs) except Exception as e: - log.error(f"Unexpected error: {str(e)}") + log.error(f"Deployment failed: {str(e)}") sys.exit(1) if __name__ == "__main__": diff --git a/modules/aws/__init__.py b/modules/aws/__init__.py index 6bdc6ca..5dec2d4 100644 --- a/modules/aws/__init__.py +++ b/modules/aws/__init__.py @@ -1,91 +1,10 @@ -# ./pulumi/modules/aws/__init__.py -""" -AWS Cloud Infrastructure Module - -Provides AWS infrastructure management capabilities including organizations, -networking, and resource provisioning with built-in compliance controls. -""" -from typing import List, Optional, Tuple, Dict, Any, TYPE_CHECKING -import pulumi +# ./modules/aws/__init__.py +"""AWS module for Konductor.""" from .types import AWSConfig -from .provider import AWSProvider -from .organization import AWSOrganization -from .resources import ResourceManager -from .networking import NetworkManager -from .iam import IAMManager -from .eks import EksManager -from .security import SecurityManager -from .exceptions import ResourceCreationError, ConfigurationError - -if TYPE_CHECKING: - from pulumi import Resource +from .deploy import AWSModule __all__ = [ - 'AWSProvider', - 'AWSOrganization', - 'ResourceManager', - 'NetworkManager', - 'IAMManager', - 'EksManager', - 'SecurityManager', 'AWSConfig', - 'create_aws_infrastructure', - 'ResourceCreationError', - 'ConfigurationError' + 'AWSModule' ] - -def create_aws_infrastructure( - config: AWSConfig, - dependencies: Optional[List[Resource]] = None -) -> Tuple[str, Resource, Dict[str, Any]]: - """ - Creates AWS infrastructure based on the provided configuration. - - This is the main entry point for AWS infrastructure creation. It orchestrates - the deployment of all AWS resources including organizations, networking, - security controls, and workload resources. - - Args: - config: AWS configuration settings including organization, networking, - security, and workload configurations - dependencies: Optional list of resources this deployment depends on - - Returns: - Tuple containing: - - Version string - - Main infrastructure resource (typically the organization) - - Dictionary of outputs including resource IDs and ARNs - - Raises: - ValueError: If configuration is invalid - ResourceCreationError: If resource creation fails - """ - try: - # Initialize provider with configuration - provider = AWSProvider(config) - - # Create managers in dependency order - security = SecurityManager(provider) - networking = NetworkManager(provider) - organization = AWSOrganization(provider) - resources = ResourceManager(provider) - iam = IAMManager(provider) - eks = EksManager(provider) - - # Deploy infrastructure - return provider.deploy( - dependencies, - managers={ - "security": security, - "networking": networking, - "organization": organization, - "resources": resources, - "iam": iam, - "eks": eks - } - ) - - except Exception as e: - pulumi.log.error(f"Failed to create AWS infrastructure: {str(e)}") - raise diff --git a/modules/aws/accounts.py b/modules/aws/accounts.py deleted file mode 100644 index e69de29..0000000 diff --git a/modules/aws/aws_iam_identity.py b/modules/aws/aws_iam_identity.py deleted file mode 100644 index 9de5ecf..0000000 --- a/modules/aws/aws_iam_identity.py +++ /dev/null @@ -1,392 +0,0 @@ -# pulumi/modules/aws/iam.py - -""" -AWS IAM Management Module - -Handles creation and management of IAM resources including: -- Users, Groups, and Roles -- Policies and Policy Attachments -- Cross-account access roles -- Service-linked roles -""" - -from typing import Dict, List, Optional, Any, TYPE_CHECKING -import json -import pulumi -import pulumi_aws as aws -from pulumi import ResourceOptions, log - -if TYPE_CHECKING: - from .types import IAMUserConfig - from .provider import AWSProvider - -class IAMManager: - """ - Manages AWS IAM resources and operations. - - This class handles: - - User and group management - - Role and policy management - - Cross-account access configuration - - Service role management - """ - - def __init__(self, provider: 'AWSProvider'): - """ - Initialize IAM manager. - - Args: - provider: AWSProvider instance for resource management - """ - self.provider = provider - - def create_user( - self, - config: IAMUserConfig, - opts: Optional[ResourceOptions] = None - ) -> aws.iam.User: - """ - Creates an IAM user with associated groups and policies. - - Args: - config: IAM user configuration - opts: Optional resource options - - Returns: - aws.iam.User: Created IAM user resource - """ - if opts is None: - opts = ResourceOptions() - - # Create the IAM user - user = aws.iam.User( - f"user-{config.name}", - name=config.name, - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - # Create login profile if email is provided - if config.email: - aws.iam.UserLoginProfile( - f"login-{config.name}", - user=user.name, - password_reset_required=True, - opts=ResourceOptions( - provider=self.provider.provider, - parent=user, - protect=True - ) - ) - - # Attach user to groups - for group_name in config.groups: - self.add_user_to_group(user, group_name) - - # Attach policies - for policy_arn in config.policies: - self.attach_user_policy(user, policy_arn) - - return user - - def create_group( - self, - name: str, - policies: Optional[List[str]] = None, - opts: Optional[ResourceOptions] = None - ) -> aws.iam.Group: - """ - Creates an IAM group with attached policies. - - Args: - name: Group name - policies: List of policy ARNs to attach - opts: Optional resource options - - Returns: - aws.iam.Group: Created IAM group - """ - if opts is None: - opts = ResourceOptions() - - group = aws.iam.Group( - f"group-{name}", - name=name, - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - if policies: - for policy_arn in policies: - aws.iam.GroupPolicyAttachment( - f"policy-{name}-{policy_arn.split('/')[-1]}", - group=group.name, - policy_arn=policy_arn, - opts=ResourceOptions( - provider=self.provider.provider, - parent=group, - protect=True - ) - ) - - return group - - def create_role( - self, - name: str, - assume_role_policy: Dict[str, Any], - policies: Optional[List[str]] = None, - description: Optional[str] = None, - opts: Optional[ResourceOptions] = None - ) -> aws.iam.Role: - """ - Creates an IAM role with specified trust and permission policies. - - Args: - name: Role name - assume_role_policy: Trust policy document - policies: List of policy ARNs to attach - description: Optional role description - opts: Optional resource options - - Returns: - aws.iam.Role: Created IAM role - """ - if opts is None: - opts = ResourceOptions() - - role = aws.iam.Role( - f"role-{name}", - name=name, - assume_role_policy=json.dumps(assume_role_policy), - description=description, - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - if policies: - for policy_arn in policies: - aws.iam.RolePolicyAttachment( - f"policy-{name}-{policy_arn.split('/')[-1]}", - role=role.name, - policy_arn=policy_arn, - opts=ResourceOptions( - provider=self.provider.provider, - parent=role, - protect=True - ) - ) - - return role - - def create_policy( - self, - name: str, - policy_document: Dict[str, Any], - description: Optional[str] = None, - opts: Optional[ResourceOptions] = None - ) -> aws.iam.Policy: - """ - Creates an IAM policy. - - Args: - name: Policy name - policy_document: Policy document - description: Optional policy description - opts: Optional resource options - - Returns: - aws.iam.Policy: Created IAM policy - """ - if opts is None: - opts = ResourceOptions() - - return aws.iam.Policy( - f"policy-{name}", - name=name, - policy=json.dumps(policy_document), - description=description, - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - def create_service_linked_role( - self, - aws_service_name: str, - description: Optional[str] = None, - opts: Optional[ResourceOptions] = None - ) -> aws.iam.ServiceLinkedRole: - """ - Creates a service-linked role for AWS services. - - Args: - aws_service_name: AWS service name (e.g., 'eks.amazonaws.com') - description: Optional role description - opts: Optional resource options - - Returns: - aws.iam.ServiceLinkedRole: Created service-linked role - """ - if opts is None: - opts = ResourceOptions() - - return aws.iam.ServiceLinkedRole( - f"slr-{aws_service_name.split('.')[0]}", - aws_service_name=aws_service_name, - description=description, - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - def create_cross_account_role( - self, - name: str, - trusted_account_id: str, - policies: List[str], - opts: Optional[ResourceOptions] = None - ) -> aws.iam.Role: - """ - Creates a role for cross-account access. - - Args: - name: Role name - trusted_account_id: AWS account ID to trust - policies: List of policy ARNs to attach - opts: Optional resource options - - Returns: - aws.iam.Role: Created cross-account role - """ - assume_role_policy = { - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Principal": { - "AWS": f"arn:aws:iam::{trusted_account_id}:root" - }, - "Action": "sts:AssumeRole" - }] - } - - return self.create_role( - name=name, - assume_role_policy=assume_role_policy, - policies=policies, - description=f"Cross-account access role for {trusted_account_id}", - opts=opts - ) - - def add_user_to_group( - self, - user: aws.iam.User, - group_name: str - ) -> aws.iam.UserGroupMembership: - """ - Adds a user to an IAM group. - - Args: - user: IAM user resource - group_name: Name of the group - - Returns: - aws.iam.UserGroupMembership: Group membership resource - """ - return aws.iam.UserGroupMembership( - f"membership-{user.name}-{group_name}", - user=user.name, - groups=[group_name], - opts=ResourceOptions( - provider=self.provider.provider, - parent=user, - protect=True - ) - ) - - def attach_user_policy( - self, - user: aws.iam.User, - policy_arn: str - ) -> aws.iam.UserPolicyAttachment: - """ - Attaches a policy to an IAM user. - - Args: - user: IAM user resource - policy_arn: ARN of the policy to attach - - Returns: - aws.iam.UserPolicyAttachment: Policy attachment resource - """ - return aws.iam.UserPolicyAttachment( - f"policy-{user.name}-{policy_arn.split('/')[-1]}", - user=user.name, - policy_arn=policy_arn, - opts=ResourceOptions( - provider=self.provider.provider, - parent=user, - protect=True - ) - ) - - def create_instance_profile( - self, - name: str, - role: aws.iam.Role, - opts: Optional[ResourceOptions] = None - ) -> aws.iam.InstanceProfile: - """ - Creates an instance profile for EC2 instances. - - Args: - name: Profile name - role: IAM role to associate - opts: Optional resource options - - Returns: - aws.iam.InstanceProfile: Created instance profile - """ - if opts is None: - opts = ResourceOptions() - - return aws.iam.InstanceProfile( - f"profile-{name}", - name=name, - role=role.name, - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - parent=role, - protect=True - ), - opts - ) - ) diff --git a/modules/aws/config.py b/modules/aws/config.py index 701ce34..7d27f09 100644 --- a/modules/aws/config.py +++ b/modules/aws/config.py @@ -1,4 +1,4 @@ -# pulumi/modules/aws/config.py +# ./modules/aws/config.py """ AWS Module Configuration @@ -26,7 +26,7 @@ generate_git_labels, collect_git_info, ) -from core.types import ComplianceConfig +from ..core.types import ComplianceConfig from .types import AWSConfig, TenantAccountConfig, validate_config from .taggable import TAGGABLE_RESOURCES diff --git a/modules/aws/deploy.py b/modules/aws/deploy.py new file mode 100644 index 0000000..fc67ed1 --- /dev/null +++ b/modules/aws/deploy.py @@ -0,0 +1,92 @@ +# ./modules/aws/deploy.py +from typing import Dict, Any, List +import pulumi +from pulumi import log +import pulumi_aws as aws + +from modules.core.interfaces import ModuleInterface, ModuleDeploymentResult +from modules.core.types import InitializationConfig +from .provider import AWSProvider +from .types import AWSConfig + +class AWSModule(ModuleInterface): + """AWS module implementation.""" + + def __init__(self): + self.name = "aws" + + def validate_config(self, config: Dict[str, Any]) -> List[str]: + """Validate AWS configuration.""" + try: + AWSConfig(**config) + return [] + except Exception as e: + return [str(e)] + + def pre_deploy_check(self) -> List[str]: + """Perform pre-deployment checks.""" + return [] + + def post_deploy_validation(self, result: ModuleDeploymentResult) -> List[str]: + """Validate deployment results.""" + return [] + + def deploy(self, config: Dict[str, Any], init_config: InitializationConfig) -> ModuleDeploymentResult: + """Deploy AWS infrastructure.""" + try: + # Parse and validate config + aws_config = AWSConfig(**config) + + # Initialize provider + provider = AWSProvider(aws_config) + + # Log provider region + log.info(f"Attempting AWS authentication in region: {provider.region}") + + try: + # Get caller identity to verify credentials + caller_identity = provider.get_caller_identity() + + # Log success + log.info(f"Successfully authenticated as: {caller_identity.arn}") + log.info(f"AWS Account ID: {caller_identity.account_id}") + + # Create example bucket to verify provider works + bucket_name = f"konductor-{init_config.stack_name}-{provider.region}" + example_bucket = aws.s3.Bucket(bucket_name, + tags=provider.get_tags(), + opts=pulumi.ResourceOptions( + provider=provider.provider, + protect=False # Don't protect example bucket + ) + ) + + return ModuleDeploymentResult( + success=True, + version="1.0.0", + resources=[provider.provider, example_bucket], + metadata={ + "caller_identity": { + "account_id": caller_identity.account_id, + "user_id": caller_identity.user_id, + "arn": caller_identity.arn + }, + "bucket_name": example_bucket.bucket + } + ) + + except Exception as e: + log.error(f"Failed to verify AWS provider: {str(e)}") + raise + + except Exception as e: + log.error(f"AWS deployment failed: {str(e)}") + return ModuleDeploymentResult( + success=False, + version="1.0.0", + errors=[str(e)] + ) + + def get_dependencies(self) -> List[str]: + """Get module dependencies.""" + return [] diff --git a/modules/aws/eks.py b/modules/aws/eks.py deleted file mode 100644 index e40006d..0000000 --- a/modules/aws/eks.py +++ /dev/null @@ -1,307 +0,0 @@ -# pulumi/modules/aws/eks.py - -""" -AWS EKS Management Module - -Handles creation and management of EKS clusters including: -- Cluster creation and configuration -- Node group management -- Add-on deployment -- Security and networking integration -""" - -from typing import Dict, List, Optional, Any, TYPE_CHECKING -import json -import pulumi -import pulumi_aws as aws -from pulumi import ResourceOptions, log - -if TYPE_CHECKING: - from .types import EksConfig, EksNodeGroupConfig, EksAddonConfig - from .provider import AWSProvider - -class EksManager: - """ - Manages EKS clusters and related resources. - - This class handles: - - Cluster provisioning - - Node group management - - Add-on deployment - - IAM integration - """ - - def __init__(self, provider: 'AWSProvider'): - """ - Initialize EKS manager. - - Args: - provider: AWSProvider instance for resource management - """ - self.provider = provider - - def create_cluster( - self, - config: 'EksConfig', - vpc_id: str, - subnet_ids: List[str], - opts: Optional[ResourceOptions] = None - ) -> aws.eks.Cluster: - """ - Creates an EKS cluster with specified configuration. - - Args: - config: EKS configuration - vpc_id: VPC ID for the cluster - subnet_ids: Subnet IDs for the cluster - opts: Optional resource options - - Returns: - aws.eks.Cluster: Created EKS cluster - """ - # Create cluster role - cluster_role = self._create_cluster_role() - - # Create KMS key for secrets encryption if enabled - kms_key = None - if config.enable_secrets_encryption: - kms_key = self.provider.security.create_kms_key( - f"eks-{config.cluster_name}-secrets", - description=f"KMS key for EKS cluster {config.cluster_name} secrets", - opts=opts - ) - - # Create the cluster - cluster = aws.eks.Cluster( - config.cluster_name, - name=config.cluster_name, - role_arn=cluster_role.arn, - version=config.kubernetes_version, - vpc_config=aws.eks.ClusterVpcConfigArgs( - subnet_ids=subnet_ids, - endpoint_private_access=config.endpoint_private_access, - endpoint_public_access=config.endpoint_public_access, - ), - encryption_config=[aws.eks.ClusterEncryptionConfigArgs( - provider=aws.eks.ProviderArgs( - key_arn=kms_key.arn - ), - resources=["secrets"] - )] if config.enable_secrets_encryption else None, - enabled_cluster_log_types=[ - "api", - "audit", - "authenticator", - "controllerManager", - "scheduler" - ], - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - # Enable IRSA if configured - if config.enable_irsa: - self._enable_irsa(cluster, config.cluster_name, opts) - - # Create node groups - for node_group_config in config.node_groups: - self.create_node_group( - cluster, - node_group_config, - subnet_ids, - opts - ) - - # Deploy add-ons - if config.addons: - self._deploy_addons(cluster, config.addons, opts) - - return cluster - - def create_node_group( - self, - cluster: aws.eks.Cluster, - config: 'EksNodeGroupConfig', - subnet_ids: List[str], - opts: Optional[ResourceOptions] = None - ) -> aws.eks.NodeGroup: - """ - Creates an EKS node group. - - Args: - cluster: EKS cluster - config: Node group configuration - subnet_ids: Subnet IDs for the node group - opts: Optional resource options - - Returns: - aws.eks.NodeGroup: Created node group - """ - # Create node role - node_role = self._create_node_role() - - # Create launch template - launch_template = self._create_launch_template( - cluster.name, - config, - opts - ) - - # Create the node group - return aws.eks.NodeGroup( - f"{cluster.name}-{config.name}", - cluster_name=cluster.name, - node_group_name=config.name, - node_role_arn=node_role.arn, - subnet_ids=subnet_ids, - scaling_config=aws.eks.NodeGroupScalingConfigArgs( - desired_size=config.desired_size, - max_size=config.max_size, - min_size=config.min_size - ), - instance_types=[config.instance_type], - capacity_type=config.capacity_type, - ami_type=config.ami_type, - disk_size=config.disk_size, - labels=config.labels, - tags=self.provider.get_tags(), - launch_template=aws.eks.NodeGroupLaunchTemplateArgs( - id=launch_template.id, - version=launch_template.latest_version - ), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True, - parent=cluster - ), - opts - ) - ) - - def _create_cluster_role(self) -> aws.iam.Role: - """Creates IAM role for EKS cluster.""" - return self.provider.iam.create_role( - "eks-cluster", - assume_role_policy={ - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Principal": { - "Service": "eks.amazonaws.com" - }, - "Action": "sts:AssumeRole" - }] - }, - policies=[ - "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy", - "arn:aws:iam::aws:policy/AmazonEKSServicePolicy" - ] - ) - - def _create_node_role(self) -> aws.iam.Role: - """Creates IAM role for EKS nodes.""" - return self.provider.iam.create_role( - "eks-node", - assume_role_policy={ - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Principal": { - "Service": "ec2.amazonaws.com" - }, - "Action": "sts:AssumeRole" - }] - }, - policies=[ - "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy", - "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy", - "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" - ] - ) - - def _create_launch_template( - self, - cluster_name: str, - config: 'EksNodeGroupConfig', - opts: Optional[ResourceOptions] = None - ) -> aws.ec2.LaunchTemplate: - """Creates launch template for node group.""" - return aws.ec2.LaunchTemplate( - f"{cluster_name}-{config.name}", - name_prefix=f"{cluster_name}-{config.name}", - block_device_mappings=[aws.ec2.LaunchTemplateBlockDeviceMappingArgs( - device_name="/dev/xvda", - ebs=aws.ec2.LaunchTemplateBlockDeviceMappingEbsArgs( - volume_size=config.disk_size, - volume_type="gp3", - encrypted=True - ) - )], - metadata_options=aws.ec2.LaunchTemplateMetadataOptionsArgs( - http_endpoint="enabled", - http_tokens="required", - http_put_response_hop_limit=2 - ), - monitoring=aws.ec2.LaunchTemplateMonitoringArgs( - enabled=True - ), - tags=self.provider.get_tags(), - opts=opts - ) - - def _enable_irsa( - self, - cluster: aws.eks.Cluster, - cluster_name: str, - opts: Optional[ResourceOptions] = None - ) -> None: - """Enables IAM Roles for Service Accounts.""" - # Create OpenID Connect Provider - oidc_url = cluster.identities[0].oidcs[0].issuer - oidc_provider = aws.iam.OpenIdConnectProvider( - f"{cluster_name}-oidc", - client_id_lists=["sts.amazonaws.com"], - thumbprint_lists=["9e99a48a9960b14926bb7f3b02e22da2b0ab7280"], - url=oidc_url, - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - parent=cluster - ), - opts - ) - ) - - def _deploy_addons( - self, - cluster: aws.eks.Cluster, - addons: 'EksAddonConfig', - opts: Optional[ResourceOptions] = None - ) -> None: - """Deploys EKS add-ons.""" - if addons.vpc_cni: - aws.eks.Addon( - f"{cluster.name}-vpc-cni", - cluster_name=cluster.name, - addon_name="vpc-cni", - resolve_conflicts="OVERWRITE", - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - parent=cluster - ), - opts - ) - ) - - # Add other add-ons similarly - # TODO: Implement remaining add-ons diff --git a/modules/aws/main.py b/modules/aws/main.py deleted file mode 100644 index e69de29..0000000 diff --git a/modules/aws/networking.py b/modules/aws/networking.py new file mode 100644 index 0000000..20014f8 --- /dev/null +++ b/modules/aws/networking.py @@ -0,0 +1,67 @@ +# ./modules/aws/networking.py +"""AWS Networking Management Module""" + +from typing import Dict, List, Optional, Any, TYPE_CHECKING +import pulumi +import pulumi_aws as aws +from pulumi import ResourceOptions, log + +if TYPE_CHECKING: + from .types import NetworkConfig + from .provider import AWSProvider + +class NetworkManager: + """Manages AWS networking resources and operations.""" + + def __init__(self, provider: 'AWSProvider'): + """Initialize Network manager.""" + self.provider = provider + + def create_vpc( + self, + name: str, + cidr_block: str, + enable_dns_hostnames: bool = True, + enable_dns_support: bool = True, + instance_tenancy: str = "default", + opts: Optional[ResourceOptions] = None + ) -> aws.ec2.Vpc: + """Creates a VPC with specified configuration.""" + if opts is None: + opts = ResourceOptions() + + vpc = aws.ec2.Vpc( + f"vpc-{name}", + cidr_block=cidr_block, + enable_dns_hostnames=enable_dns_hostnames, + enable_dns_support=enable_dns_support, + instance_tenancy=instance_tenancy, + tags=self.provider.get_tags(), + opts=ResourceOptions.merge( + ResourceOptions( + provider=self.provider.provider, + protect=True + ), + opts + ) + ) + + return vpc + + def deploy_network_infrastructure(self) -> Dict[str, Any]: + """Deploys networking infrastructure and returns outputs.""" + try: + # Create VPC + vpc = self.create_vpc( + "main", + self.provider.config.network.vpc_cidr + ) + + return { + "vpc_id": vpc.id, + "subnet_ids": [] # Add subnet creation later + } + + except Exception as e: + log.error(f"Failed to deploy network infrastructure: {str(e)}") + raise diff --git a/modules/aws/networks.py b/modules/aws/networks.py deleted file mode 100644 index 8a4cf9a..0000000 --- a/modules/aws/networks.py +++ /dev/null @@ -1,554 +0,0 @@ -# pulumi/modules/aws/networking.py - -""" -AWS Networking Management Module - -Handles creation and management of AWS networking resources including: -- VPCs and subnets -- Route tables and routes -- Security groups and rules -- Internet and NAT gateways -- Network ACLs -- VPC endpoints -""" - -from typing import Dict, List, Optional, Any, Tuple, TYPE_CHECKING -import pulumi -import pulumi_aws as aws -from pulumi import ResourceOptions, log -from .security import SecurityManager - -if TYPE_CHECKING: - from .types import NetworkConfig - from .provider import AWSProvider - -class NetworkManager: - """ - Manages AWS networking resources and operations. - - This class handles: - - VPC and subnet management - - Routing configuration - - Security group management - - Gateway provisioning - - Network ACL configuration - """ - - def __init__(self, provider: 'AWSProvider'): - """ - Initialize Network manager. - - Args: - provider: AWSProvider instance for resource management - """ - self.provider = provider - - def create_vpc( - self, - name: str, - cidr_block: str, - enable_dns_hostnames: bool = True, - enable_dns_support: bool = True, - instance_tenancy: str = "default", - opts: Optional[ResourceOptions] = None - ) -> aws.ec2.Vpc: - """ - Creates a VPC with the specified configuration. - - Args: - name: VPC name - cidr_block: CIDR block for the VPC - enable_dns_hostnames: Enable DNS hostnames - enable_dns_support: Enable DNS support - instance_tenancy: Default instance tenancy - opts: Optional resource options - - Returns: - aws.ec2.Vpc: Created VPC resource - """ - # Add flow logs configuration - def enable_vpc_flow_logs(self, vpc: aws.ec2.Vpc) -> aws.ec2.FlowLog: - log_group = aws.cloudwatch.LogGroup(...) - return aws.ec2.FlowLog( - f"flow-log-{vpc.id}", - vpc_id=vpc.id, - traffic_type="ALL", - log_destination=log_group.arn, - opts=ResourceOptions( - provider=self.provider.provider, - parent=vpc - ) - ) - - if opts is None: - opts = ResourceOptions() - - vpc = aws.ec2.Vpc( - f"vpc-{name}", - cidr_block=cidr_block, - enable_dns_hostnames=enable_dns_hostnames, - enable_dns_support=enable_dns_support, - instance_tenancy=instance_tenancy, - tags={ - **self.provider.get_tags(), - "Name": f"vpc-{name}" - }, - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - # Enable VPC flow logs by default - self.enable_vpc_flow_logs(vpc) - - return vpc - - def enable_vpc_flow_logs( - self, - vpc: aws.ec2.Vpc, - retention_days: int = 7 - ) -> aws.ec2.FlowLog: - """ - Enables VPC flow logs. - - Args: - vpc: VPC resource - retention_days: Log retention period in days - - Returns: - aws.ec2.FlowLog: Flow log resource - """ - # Create log group for flow logs - log_group = aws.cloudwatch.LogGroup( - f"flow-logs-{vpc.id}", - retention_in_days=retention_days, - tags=self.provider.get_tags(), - opts=ResourceOptions( - provider=self.provider.provider, - parent=vpc - ) - ) - - # Create IAM role for flow logs - assume_role_policy = { - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Principal": { - "Service": "vpc-flow-logs.amazonaws.com" - }, - "Action": "sts:AssumeRole" - }] - } - - role = aws.iam.Role( - f"flow-logs-role-{vpc.id}", - assume_role_policy=pulumi.Output.from_input(assume_role_policy).apply(lambda x: pulumi.Output.json_dumps(x)), - tags=self.provider.get_tags(), - opts=ResourceOptions( - provider=self.provider.provider, - parent=vpc - ) - ) - - # Attach policy to role - aws.iam.RolePolicy( - f"flow-logs-policy-{vpc.id}", - role=role.id, - policy=pulumi.Output.all(log_group_arn=log_group.arn).apply( - lambda args: pulumi.Output.json_dumps({ - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Action": [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents", - "logs:DescribeLogGroups", - "logs:DescribeLogStreams" - ], - "Resource": [ - args["log_group_arn"], - f"{args['log_group_arn']}:*" - ] - }] - }) - ), - opts=ResourceOptions( - provider=self.provider.provider, - parent=role - ) - ) - - # Create flow log - return aws.ec2.FlowLog( - f"flow-log-{vpc.id}", - vpc_id=vpc.id, - traffic_type="ALL", - iam_role_arn=role.arn, - log_destination=log_group.arn, - tags=self.provider.get_tags(), - opts=ResourceOptions( - provider=self.provider.provider, - parent=vpc - ) - ) - - def create_subnet( - self, - name: str, - vpc_id: pulumi.Input[str], - cidr_block: str, - availability_zone: str, - map_public_ip: bool = False, - opts: Optional[ResourceOptions] = None - ) -> aws.ec2.Subnet: - """ - Creates a subnet in the specified VPC. - - Args: - name: Subnet name - vpc_id: VPC ID - cidr_block: CIDR block for the subnet - availability_zone: AZ for the subnet - map_public_ip: Auto-assign public IPs - opts: Optional resource options - - Returns: - aws.ec2.Subnet: Created subnet resource - """ - if opts is None: - opts = ResourceOptions() - - return aws.ec2.Subnet( - f"subnet-{name}", - vpc_id=vpc_id, - cidr_block=cidr_block, - availability_zone=availability_zone, - map_public_ip_on_launch=map_public_ip, - tags={ - **self.provider.get_tags(), - "Name": f"subnet-{name}" - }, - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - def create_internet_gateway( - self, - name: str, - vpc_id: pulumi.Input[str], - opts: Optional[ResourceOptions] = None - ) -> aws.ec2.InternetGateway: - """ - Creates and attaches an internet gateway to a VPC. - - Args: - name: Gateway name - vpc_id: VPC ID - opts: Optional resource options - - Returns: - aws.ec2.InternetGateway: Created internet gateway - """ - if opts is None: - opts = ResourceOptions() - - return aws.ec2.InternetGateway( - f"igw-{name}", - vpc_id=vpc_id, - tags={ - **self.provider.get_tags(), - "Name": f"igw-{name}" - }, - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - def create_nat_gateway( - self, - name: str, - subnet_id: pulumi.Input[str], - opts: Optional[ResourceOptions] = None - ) -> Tuple[aws.ec2.Eip, aws.ec2.NatGateway]: - """ - Creates a NAT gateway with an Elastic IP. - - Args: - name: Gateway name - subnet_id: Subnet ID for the NAT gateway - opts: Optional resource options - - Returns: - Tuple containing: - - Elastic IP resource - - NAT Gateway resource - """ - if opts is None: - opts = ResourceOptions() - - eip = aws.ec2.Eip( - f"eip-{name}", - vpc=True, - tags={ - **self.provider.get_tags(), - "Name": f"eip-{name}" - }, - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - nat_gateway = aws.ec2.NatGateway( - f"nat-{name}", - subnet_id=subnet_id, - allocation_id=eip.id, - tags={ - **self.provider.get_tags(), - "Name": f"nat-{name}" - }, - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True, - depends_on=[eip] - ), - opts - ) - ) - - return eip, nat_gateway - - def create_route_table( - self, - name: str, - vpc_id: pulumi.Input[str], - routes: Optional[List[Dict[str, Any]]] = None, - opts: Optional[ResourceOptions] = None - ) -> aws.ec2.RouteTable: - """ - Creates a route table with specified routes. - - Args: - name: Route table name - vpc_id: VPC ID - routes: List of route configurations - opts: Optional resource options - - Returns: - aws.ec2.RouteTable: Created route table - """ - if opts is None: - opts = ResourceOptions() - - route_table = aws.ec2.RouteTable( - f"rt-{name}", - vpc_id=vpc_id, - tags={ - **self.provider.get_tags(), - "Name": f"rt-{name}" - }, - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - if routes: - for idx, route in enumerate(routes): - aws.ec2.Route( - f"route-{name}-{idx}", - route_table_id=route_table.id, - destination_cidr_block=route.get("destination_cidr_block"), - gateway_id=route.get("gateway_id"), - nat_gateway_id=route.get("nat_gateway_id"), - opts=ResourceOptions( - provider=self.provider.provider, - parent=route_table - ) - ) - - return route_table - - def create_security_group( - self, - name: str, - vpc_id: pulumi.Input[str], - description: str, - ingress_rules: Optional[List[Dict[str, Any]]] = None, - egress_rules: Optional[List[Dict[str, Any]]] = None, - opts: Optional[ResourceOptions] = None - ) -> aws.ec2.SecurityGroup: - """ - Creates a security group with specified rules. - - Args: - name: Security group name - vpc_id: VPC ID - description: Security group description - ingress_rules: List of ingress rule configurations - egress_rules: List of egress rule configurations - opts: Optional resource options - - Returns: - aws.ec2.SecurityGroup: Created security group - """ - if opts is None: - opts = ResourceOptions() - - security_group = aws.ec2.SecurityGroup( - f"sg-{name}", - vpc_id=vpc_id, - description=description, - tags={ - **self.provider.get_tags(), - "Name": f"sg-{name}" - }, - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - if ingress_rules: - for idx, rule in enumerate(ingress_rules): - aws.ec2.SecurityGroupRule( - f"sgr-{name}-ingress-{idx}", - type="ingress", - security_group_id=security_group.id, - protocol=rule.get("protocol", "tcp"), - from_port=rule.get("from_port"), - to_port=rule.get("to_port"), - cidr_blocks=rule.get("cidr_blocks"), - source_security_group_id=rule.get("source_security_group_id"), - opts=ResourceOptions( - provider=self.provider.provider, - parent=security_group - ) - ) - - if egress_rules: - for idx, rule in enumerate(egress_rules): - aws.ec2.SecurityGroupRule( - f"sgr-{name}-egress-{idx}", - type="egress", - security_group_id=security_group.id, - protocol=rule.get("protocol", "-1"), - from_port=rule.get("from_port", 0), - to_port=rule.get("to_port", 0), - cidr_blocks=rule.get("cidr_blocks", ["0.0.0.0/0"]), - opts=ResourceOptions( - provider=self.provider.provider, - parent=security_group - ) - ) - - return security_group - - def create_vpc_endpoint( - self, - name: str, - vpc_id: pulumi.Input[str], - service_name: str, - subnet_ids: Optional[List[pulumi.Input[str]]] = None, - security_group_ids: Optional[List[pulumi.Input[str]]] = None, - vpc_endpoint_type: str = "Interface", - private_dns_enabled: bool = True, - opts: Optional[ResourceOptions] = None - ) -> aws.ec2.VpcEndpoint: - """ - Creates a VPC endpoint for AWS services. - - Args: - name: Endpoint name - vpc_id: VPC ID - service_name: AWS service name - subnet_ids: List of subnet IDs for the endpoint - security_group_ids: List of security group IDs - vpc_endpoint_type: Endpoint type (Interface/Gateway) - private_dns_enabled: Enable private DNS - opts: Optional resource options - - Returns: - aws.ec2.VpcEndpoint: Created VPC endpoint - """ - if opts is None: - opts = ResourceOptions() - - return aws.ec2.VpcEndpoint( - f"vpce-{name}", - vpc_id=vpc_id, - service_name=service_name, - vpc_endpoint_type=vpc_endpoint_type, - subnet_ids=subnet_ids, - security_group_ids=security_group_ids, - private_dns_enabled=private_dns_enabled, - tags={ - **self.provider.get_tags(), - "Name": f"vpce-{name}" - }, - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - def deploy_network_infrastructure(self) -> Dict[str, Any]: - """Deploys networking infrastructure and returns outputs.""" - try: - # Create VPC - vpc = self.create_vpc( - "main", - self.provider.config.network.vpc_cidr - ) - - # Create subnets - subnets = [] - for i, az in enumerate(self.provider.config.network.availability_zones): - subnet = self.create_subnet( - f"subnet-{i}", - vpc.id, - self.provider.config.network.subnet_cidrs["private"][i], - az, - opts=ResourceOptions( - provider=self.provider.provider, - parent=vpc - ) - ) - subnets.append(subnet) - - return { - "vpc_id": vpc.id, - "subnet_ids": [s.id for s in subnets] - } - - except Exception as e: - log.error(f"Failed to deploy network infrastructure: {str(e)}") - raise diff --git a/modules/aws/organization.py b/modules/aws/organization.py new file mode 100644 index 0000000..b27b0f0 --- /dev/null +++ b/modules/aws/organization.py @@ -0,0 +1,88 @@ +# ./modules/aws/organization.py +"""AWS Organizations management and operations.""" +from typing import Dict, List, Optional, Tuple, Any, TYPE_CHECKING +import pulumi +import pulumi_aws as aws +from pulumi import ResourceOptions, log + +if TYPE_CHECKING: + from .provider import AWSProvider + +class AWSOrganization: + """ + Manages AWS Organizations resources and operations. + """ + + def __init__(self, provider: 'AWSProvider'): + """ + Initialize AWS Organizations manager. + + Args: + provider: AWSProvider instance for resource management + """ + self.provider = provider + self._organization: Optional[aws.organizations.Organization] = None + self._org_data: Optional[aws.organizations.GetOrganizationResult] = None + + def get_or_create(self) -> Tuple[aws.organizations.Organization, aws.organizations.GetOrganizationResult]: + """ + Retrieves existing AWS Organization or creates a new one. + + Returns: + Tuple containing: + - Organization resource + - Organization data + """ + try: + log.debug("Attempting to get organization") + # Try to get existing organization + org_data = aws.organizations.get_organization( + opts=pulumi.InvokeOptions(provider=self.provider.provider) + ) + log.info(f"Found existing Organization with ID: {org_data.id}") + + # Create resource reference to existing organization + organization = aws.organizations.Organization.get( + "existing_organization", + id=org_data.id, + opts=ResourceOptions( + provider=self.provider.provider, + protect=True + ) + ) + return organization, org_data + + except Exception as e: + log.warn(f"No existing organization found, creating new: {str(e)}") + + # Create new organization with all features enabled + organization = aws.organizations.Organization( + "aws_organization", + feature_set="ALL", + aws_service_access_principals=[ + "cloudtrail.amazonaws.com", + "config.amazonaws.com", + "sso.amazonaws.com" + ], + enabled_policy_types=[ + "SERVICE_CONTROL_POLICY", + "TAG_POLICY" + ], + opts=ResourceOptions( + provider=self.provider.provider, + protect=True + ) + ) + + # Get organization data after creation + org_data = aws.organizations.get_organization( + opts=pulumi.InvokeOptions(provider=self.provider.provider) + ) + + return organization, org_data + + def get_root_id(self, org_data: Dict[str, Any]) -> str: + """Get the root ID from organization data.""" + if not org_data.get("roots"): + raise ValueError("Organization roots not found in org data") + return org_data["roots"][0]["id"] diff --git a/modules/aws/organizations.py b/modules/aws/organizations.py deleted file mode 100644 index 287f55a..0000000 --- a/modules/aws/organizations.py +++ /dev/null @@ -1,413 +0,0 @@ -# pulumi/modules/aws/organization.py - -"""AWS Organizations management and operations.""" -from typing import Dict, List, Optional, Tuple, Any, TYPE_CHECKING -import pulumi -import pulumi_aws as aws -from pulumi import ResourceOptions, log - -if TYPE_CHECKING: - from .types import TenantAccountConfig - from .provider import AWSProvider - from .resources import create_tenant_account, assume_role_in_tenant_account, deploy_tenant_resources - -class AWSOrganization: - """ - Manages AWS Organizations resources and operations. - - This class handles: - - Organization creation and management - - Organizational Unit (OU) operations - - Account management - - Control Tower integration - """ - - def __init__(self, provider: 'AWSProvider'): - """ - Initialize AWS Organizations manager. - - Args: - provider: AWSProvider instance for resource management - """ - self.provider = provider - self._organization: Optional[aws.organizations.Organization] = None - self._org_data: Optional[aws.organizations.GetOrganizationResult] = None - - def get_or_create(self) -> Tuple[aws.organizations.Organization, aws.organizations.GetOrganizationResult]: - """ - Retrieves existing AWS Organization or creates a new one. - - Returns: - Tuple containing: - - Organization resource - - Organization data - - Raises: - Exception: If unable to retrieve or create organization - """ - try: - # Try to get existing organization - org_data = aws.organizations.get_organization( - opts=pulumi.InvokeOptions(provider=self.provider.provider) - ) - log.info(f"Found existing Organization with ID: {org_data.id}") - - # Create resource reference to existing organization - organization = aws.organizations.Organization.get( - "existing_organization", - id=org_data.id, - opts=ResourceOptions( - provider=self.provider.provider, - protect=True - ) - ) - return organization, org_data - - except Exception as e: - log.warn(f"No existing organization found, creating new: {str(e)}") - - # Create new organization with all features enabled - organization = aws.organizations.Organization( - "aws_organization", - feature_set="ALL", - aws_service_access_principals=[ - "cloudtrail.amazonaws.com", - "config.amazonaws.com", - "sso.amazonaws.com" - ], - enabled_policy_types=[ - "SERVICE_CONTROL_POLICY", - "TAG_POLICY" - ], - opts=ResourceOptions( - provider=self.provider.provider, - protect=True - ) - ) - - # Get organization data after creation - org_data = aws.organizations.get_organization( - opts=pulumi.InvokeOptions(provider=self.provider.provider) - ) - - return organization, org_data - - def get_root_id(self, org_data: Dict[str, Any]) -> str: - """ - Get the root ID from organization data. - - Args: - org_data: Organization data dictionary - - Returns: - str: Root ID of the organization - """ - if not org_data.get("roots"): - raise ValueError("Organization roots not found in org data") - return org_data["roots"][0]["id"] - - def create_units( - self, - organization: aws.organizations.Organization, - root_id: str, - unit_names: List[str] - ) -> Dict[str, aws.organizations.OrganizationalUnit]: - """ - Creates Organizational Units under the organization root. - - Args: - organization: The AWS Organization resource - root_id: The root ID to create OUs under - unit_names: List of OU names to create - - Returns: - Dict[str, OrganizationalUnit]: Created OUs mapped by name - - Raises: - ValueError: If root_id is invalid - """ - if not root_id: - raise ValueError("Root ID is required to create Organizational Units") - - organizational_units = {} - - for unit_name in unit_names: - # Create OU with standard naming - ou = aws.organizations.OrganizationalUnit( - f"ou_{unit_name.lower()}", - name=unit_name, - parent_id=root_id, - tags=self.provider.get_tags(), - opts=ResourceOptions( - provider=self.provider.provider, - parent=organization, - protect=True - ) - ) - organizational_units[unit_name] = ou - - # Create default policies for the OU - self._create_ou_policies(ou, unit_name) - - return organizational_units - - def _create_ou_policies( - self, - ou: aws.organizations.OrganizationalUnit, - ou_name: str - ) -> None: - """ - Creates default policies for an Organizational Unit. - - Args: - ou: The OU to create policies for - ou_name: Name of the OU for policy naming - """ - # Create Service Control Policy - scp = aws.organizations.Policy( - f"scp_{ou_name.lower()}", - content=self._get_default_scp_content(ou_name), - name=f"{ou_name}-BaselinePolicy", - type="SERVICE_CONTROL_POLICY", - tags=self.provider.get_tags(), - opts=ResourceOptions( - provider=self.provider.provider, - parent=ou, - protect=True - ) - ) - - # Attach policy to OU - aws.organizations.PolicyAttachment( - f"scp_attachment_{ou_name.lower()}", - policy_id=scp.id, - target_id=ou.id, - opts=ResourceOptions( - provider=self.provider.provider, - parent=scp, - protect=True - ) - ) - - def _get_default_scp_content(self, ou_name: str) -> str: - """ - Gets default SCP content based on OU type. - - Args: - ou_name: Name of the OU to determine policy content - - Returns: - str: JSON policy content - """ - if ou_name == "Security": - return """{ - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "RequireIMDSv2", - "Effect": "Deny", - "Action": "ec2:RunInstances", - "Resource": "arn:aws:ec2:*:*:instance/*", - "Condition": { - "StringNotEquals": { - "ec2:MetadataHttpTokens": "required" - } - } - } - ] - }""" - elif ou_name == "Workloads": - return """{ - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "DenyUnencryptedVolumes", - "Effect": "Deny", - "Action": "ec2:CreateVolume", - "Resource": "*", - "Condition": { - "Bool": { - "aws:SecureTransport": "false" - } - } - } - ] - }""" - else: - return """{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": "*", - "Resource": "*" - } - ] - }""" - - def create_account( - self, - name: str, - email: str, - parent_id: str, - role_name: str = "OrganizationAccountAccessRole" - ) -> aws.organizations.Account: - """ - Creates a new AWS account in the organization. - - Args: - name: Account name - email: Account root email - parent_id: Parent OU ID - role_name: IAM role name for account access - - Returns: - aws.organizations.Account: Created account resource - """ - account = aws.organizations.Account( - f"account_{name.lower()}", - name=name, - email=email, - parent_id=parent_id, - role_name=role_name, - tags=self.provider.get_tags(), - opts=ResourceOptions( - provider=self.provider.provider, - protect=True - ) - ) - - # Wait for account to be created before returning - account.id.apply(lambda id: log.info(f"Created account {name} with ID: {id}")) - - return account - - def move_account( - self, - account: aws.organizations.Account, - source_parent: str, - destination_parent: str - ) -> None: - """ - Moves an AWS account between organizational units. - - Args: - account: Account to move - source_parent: Source parent ID - destination_parent: Destination parent ID - """ - aws.organizations.AccountParent( - f"move_{account.name}", - account_id=account.id, - parent_id=destination_parent, - opts=ResourceOptions( - provider=self.provider.provider, - parent=account, - protect=True, - replace_on_changes=["parent_id"] - ) - ) - - def enable_aws_service_access( - self, - service_principal: str - ) -> aws.organizations.DelegatedService: - """ - Enables AWS service access in the organization. - - Args: - service_principal: Service principal to enable (e.g. 'config.amazonaws.com') - - Returns: - aws.organizations.DelegatedService: Service access resource - """ - return aws.organizations.DelegatedService( - f"service_access_{service_principal.split('.')[0]}", - service_principal=service_principal, - opts=ResourceOptions( - provider=self.provider.provider, - protect=True - ) - ) - - def enable_policy_type( - self, - policy_type: str - ) -> aws.organizations.OrganizationalPolicyAttachment: - """ - Enables a policy type in the organization. - - Args: - policy_type: Type of policy to enable (e.g. 'SERVICE_CONTROL_POLICY') - - Returns: - aws.organizations.OrganizationalPolicyAttachment: Policy type enablement - """ - return aws.organizations.OrganizationalPolicyAttachment( - f"enable_policy_{policy_type.lower()}", - policy_type=policy_type, - target_id=self._org_data.roots[0].id if self._org_data else None, - opts=ResourceOptions( - provider=self.provider.provider, - protect=True - ) - ) - -def deploy_tenant_infrastructure( - tenant_config: TenantAccountConfig, - parent_provider: aws.Provider, - organization_id: str, - depends_on: Optional[List[pulumi.Resource]] = None -) -> Dict[str, Any]: - """ - Deploys infrastructure for a tenant account. - - Args: - tenant_config: Tenant account configuration - parent_provider: Parent AWS provider - organization_id: AWS Organization ID - depends_on: Optional resource dependencies - - Returns: - Dict[str, Any]: Tenant infrastructure outputs - - TODO: - - Implement tenant-specific compliance controls - - Add tenant resource monitoring - - Enhance tenant isolation mechanisms - - Add tenant cost tracking - - Implement tenant backup strategies - """ - try: - # Create tenant account - tenant_account = create_tenant_account( - tenant_config, - parent_provider, - organization_id, - depends_on - ) - - # Assume role in tenant account - tenant_provider = assume_role_in_tenant_account( - tenant_account, - "OrganizationAccountAccessRole", - tenant_config.region, - parent_provider - ) - - # Deploy tenant resources - tenant_resources = deploy_tenant_resources( - tenant_provider, - tenant_account, - tenant_config - ) - - return { - "account_id": tenant_account.id, - "account_arn": tenant_account.arn, - "resources": tenant_resources - } - - except Exception as e: - pulumi.log.error(f"Error deploying tenant infrastructure: {str(e)}") - raise diff --git a/modules/aws/provider.py b/modules/aws/provider.py new file mode 100644 index 0000000..907d3e4 --- /dev/null +++ b/modules/aws/provider.py @@ -0,0 +1,139 @@ +# ./modules/aws/provider.py +from typing import Optional, Dict, Any +import pulumi +import pulumi_aws as aws +from pulumi import ResourceOptions, Config, log +import os + +from .types import AWSConfig + +class AWSProvider: + """Manages AWS provider initialization and configuration.""" + + def __init__(self, config: AWSConfig): + """ + Initialize AWS provider with configuration. + + Args: + config: AWS configuration settings + """ + log.debug("Initializing AWSProvider") + self.config = config + self._provider: Optional[aws.Provider] = None + self._tags: Dict[str, str] = {} + self._region: str = "" + + try: + log.debug("Setting up AWS provider configuration") + # Get AWS credentials and region with proper fallbacks + pulumi_config = Config("aws") + + # Retrieve AWS region + aws_region = ( + os.getenv("AWS_REGION") or + pulumi_config.get("region") or + self.config.region + ) + if not aws_region: + raise ValueError("AWS region is not specified.") + + # Retrieve AWS access key and secret access key + access_key_id = os.getenv("AWS_ACCESS_KEY_ID") or pulumi_config.get_secret("access_key_id") + secret_access_key = os.getenv("AWS_SECRET_ACCESS_KEY") or pulumi_config.get_secret("secret_access_key") + + # Retrieve AWS profile if access keys are not provided + aws_profile = None + if not access_key_id and not secret_access_key: + aws_profile = os.getenv("AWS_PROFILE") or pulumi_config.get("profile") or self.config.profile + if not aws_profile: + raise ValueError("AWS credentials not provided. Set access keys or profile.") + + self._region = aws_region + + # Initialize AWS provider with the appropriate authentication method + provider_args = { + "region": aws_region, + } + if access_key_id and secret_access_key: + provider_args.update({ + "access_key": access_key_id, + "secret_key": secret_access_key, + }) + log.debug("Using AWS access key and secret key for authentication.") + elif aws_profile: + provider_args["profile"] = aws_profile + log.debug(f"Using AWS profile '{aws_profile}' for authentication.") + else: + raise ValueError("AWS credentials not provided. Set access keys or profile.") + + log.debug(f"Created provider with args: {provider_args}") + self._provider = aws.Provider("aws-provider", **provider_args) + log.debug("AWS Provider instance created successfully") + + log.info(f"AWS Provider initialized in region: {aws_region}") + + except Exception as e: + log.error(f"Failed to initialize AWS provider: {str(e)}") + log.debug(f"Provider initialization failed with config: {self.config}") + raise + + @property + def provider(self) -> aws.Provider: + """ + Get the AWS provider instance. + + Returns: + aws.Provider: Initialized AWS provider + + Raises: + RuntimeError: If provider is not initialized + """ + if not self._provider: + raise RuntimeError("AWS Provider not initialized") + return self._provider + + @property + def region(self) -> str: + """ + Get the AWS region. + + Returns: + str: The configured AWS region + """ + return self._region + + def get_caller_identity(self) -> aws.GetCallerIdentityResult: + """ + Get AWS caller identity information. + + Returns: + aws.GetCallerIdentityResult: Caller identity information + + Raises: + Exception: If caller identity check fails + """ + try: + if not self._provider: + raise RuntimeError("AWS Provider not initialized") + + # Simplest possible version - no options merging + return aws.get_caller_identity() + + except Exception as e: + log.error(f"Failed to get caller identity: {str(e)}") + raise + + def get_tags(self) -> Dict[str, str]: + """ + Get AWS resource tags. + + Returns: + Dict[str, str]: Combined resource tags + """ + if not self._tags: + self._tags = { + "managed-by": "konductor", + "environment": self.config.profile or "default", + "region": self._region + } + return self._tags diff --git a/modules/aws/provider/__init__.py b/modules/aws/provider/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/modules/aws/provider/config.py b/modules/aws/provider/config.py deleted file mode 100644 index e69de29..0000000 diff --git a/modules/aws/provider/main.py b/modules/aws/provider/main.py deleted file mode 100644 index e69de29..0000000 diff --git a/modules/aws/provider/metadata.py b/modules/aws/provider/metadata.py deleted file mode 100644 index e69de29..0000000 diff --git a/modules/aws/provider/resource_tags.py b/modules/aws/provider/resource_tags.py deleted file mode 100644 index e69de29..0000000 diff --git a/modules/aws/provider/tagable_resources_list.py b/modules/aws/provider/tagable_resources_list.py deleted file mode 100644 index d124f33..0000000 --- a/modules/aws/provider/tagable_resources_list.py +++ /dev/null @@ -1,701 +0,0 @@ -# This file is generated by scripts/taggable_aws_resources.py -# Do not edit manually. - -TAGGABLE_RESOURCES = [ - "aws:accessanalyzer/analyzer:Analyzer", - "aws:acm/certificate:Certificate", - "aws:acmpca/certificateAuthority:CertificateAuthority", - "aws:alb/listener:Listener", - "aws:alb/listenerRule:ListenerRule", - "aws:alb/loadBalancer:LoadBalancer", - "aws:alb/targetGroup:TargetGroup", - "aws:amp/scraper:Scraper", - "aws:amp/workspace:Workspace", - "aws:amplify/app:App", - "aws:amplify/branch:Branch", - "aws:apigateway/apiKey:ApiKey", - "aws:apigateway/clientCertificate:ClientCertificate", - "aws:apigateway/domainName:DomainName", - "aws:apigateway/restApi:RestApi", - "aws:apigateway/stage:Stage", - "aws:apigateway/usagePlan:UsagePlan", - "aws:apigateway/vpcLink:VpcLink", - "aws:apigatewayv2/api:Api", - "aws:apigatewayv2/domainName:DomainName", - "aws:apigatewayv2/stage:Stage", - "aws:apigatewayv2/vpcLink:VpcLink", - "aws:appautoscaling/target:Target", - "aws:appconfig/application:Application", - "aws:appconfig/configurationProfile:ConfigurationProfile", - "aws:appconfig/deployment:Deployment", - "aws:appconfig/deploymentStrategy:DeploymentStrategy", - "aws:appconfig/environment:Environment", - "aws:appconfig/eventIntegration:EventIntegration", - "aws:appconfig/extension:Extension", - "aws:appfabric/appAuthorization:AppAuthorization", - "aws:appfabric/appBundle:AppBundle", - "aws:appfabric/ingestion:Ingestion", - "aws:appfabric/ingestionDestination:IngestionDestination", - "aws:appflow/flow:Flow", - "aws:appintegrations/dataIntegration:DataIntegration", - "aws:applicationinsights/application:Application", - "aws:appmesh/gatewayRoute:GatewayRoute", - "aws:appmesh/mesh:Mesh", - "aws:appmesh/route:Route", - "aws:appmesh/virtualGateway:VirtualGateway", - "aws:appmesh/virtualNode:VirtualNode", - "aws:appmesh/virtualRouter:VirtualRouter", - "aws:appmesh/virtualService:VirtualService", - "aws:apprunner/autoScalingConfigurationVersion:AutoScalingConfigurationVersion", - "aws:apprunner/connection:Connection", - "aws:apprunner/observabilityConfiguration:ObservabilityConfiguration", - "aws:apprunner/service:Service", - "aws:apprunner/vpcConnector:VpcConnector", - "aws:apprunner/vpcIngressConnection:VpcIngressConnection", - "aws:appstream/fleet:Fleet", - "aws:appstream/imageBuilder:ImageBuilder", - "aws:appstream/stack:Stack", - "aws:appsync/graphQLApi:GraphQLApi", - "aws:athena/dataCatalog:DataCatalog", - "aws:athena/workgroup:Workgroup", - "aws:auditmanager/assessment:Assessment", - "aws:auditmanager/control:Control", - "aws:auditmanager/framework:Framework", - "aws:autoscaling/group:Group", - "aws:backup/framework:Framework", - "aws:backup/logicallyAirGappedVault:LogicallyAirGappedVault", - "aws:backup/plan:Plan", - "aws:backup/reportPlan:ReportPlan", - "aws:backup/vault:Vault", - "aws:batch/computeEnvironment:ComputeEnvironment", - "aws:batch/jobDefinition:JobDefinition", - "aws:batch/jobQueue:JobQueue", - "aws:batch/schedulingPolicy:SchedulingPolicy", - "aws:bcmdata/export:Export", - "aws:bedrock/agentAgent:AgentAgent", - "aws:bedrock/agentAgentAlias:AgentAgentAlias", - "aws:bedrock/agentKnowledgeBase:AgentKnowledgeBase", - "aws:bedrock/customModel:CustomModel", - "aws:bedrock/guardrail:Guardrail", - "aws:bedrock/provisionedModelThroughput:ProvisionedModelThroughput", - "aws:budgets/budget:Budget", - "aws:budgets/budgetAction:BudgetAction", - "aws:cfg/aggregateAuthorization:AggregateAuthorization", - "aws:cfg/configurationAggregator:ConfigurationAggregator", - "aws:cfg/rule:Rule", - "aws:chatbot/slackChannelConfiguration:SlackChannelConfiguration", - "aws:chatbot/teamsChannelConfiguration:TeamsChannelConfiguration", - "aws:chime/sdkvoiceSipMediaApplication:SdkvoiceSipMediaApplication", - "aws:chime/sdkvoiceVoiceProfileDomain:SdkvoiceVoiceProfileDomain", - "aws:chime/voiceConnector:VoiceConnector", - "aws:chimesdkmediapipelines/mediaInsightsPipelineConfiguration:MediaInsightsPipelineConfiguration", - "aws:cleanrooms/collaboration:Collaboration", - "aws:cleanrooms/configuredTable:ConfiguredTable", - "aws:cloud9/environmentEC2:EnvironmentEC2", - "aws:cloudformation/stack:Stack", - "aws:cloudformation/stackSet:StackSet", - "aws:cloudfront/distribution:Distribution", - "aws:cloudhsmv2/cluster:Cluster", - "aws:cloudtrail/eventDataStore:EventDataStore", - "aws:cloudtrail/trail:Trail", - "aws:cloudwatch/compositeAlarm:CompositeAlarm", - "aws:cloudwatch/eventBus:EventBus", - "aws:cloudwatch/eventRule:EventRule", - "aws:cloudwatch/internetMonitor:InternetMonitor", - "aws:cloudwatch/logDestination:LogDestination", - "aws:cloudwatch/logGroup:LogGroup", - "aws:cloudwatch/metricAlarm:MetricAlarm", - "aws:cloudwatch/metricStream:MetricStream", - "aws:codeartifact/domain:Domain", - "aws:codeartifact/repository:Repository", - "aws:codebuild/fleet:Fleet", - "aws:codebuild/project:Project", - "aws:codebuild/reportGroup:ReportGroup", - "aws:codecommit/repository:Repository", - "aws:codedeploy/application:Application", - "aws:codedeploy/deploymentGroup:DeploymentGroup", - "aws:codeguruprofiler/profilingGroup:ProfilingGroup", - "aws:codegurureviewer/repositoryAssociation:RepositoryAssociation", - "aws:codepipeline/customActionType:CustomActionType", - "aws:codepipeline/pipeline:Pipeline", - "aws:codepipeline/webhook:Webhook", - "aws:codestarconnections/connection:Connection", - "aws:codestarnotifications/notificationRule:NotificationRule", - "aws:cognito/identityPool:IdentityPool", - "aws:cognito/userPool:UserPool", - "aws:comprehend/documentClassifier:DocumentClassifier", - "aws:comprehend/entityRecognizer:EntityRecognizer", - "aws:connect/contactFlow:ContactFlow", - "aws:connect/contactFlowModule:ContactFlowModule", - "aws:connect/hoursOfOperation:HoursOfOperation", - "aws:connect/instance:Instance", - "aws:connect/phoneNumber:PhoneNumber", - "aws:connect/queue:Queue", - "aws:connect/quickConnect:QuickConnect", - "aws:connect/routingProfile:RoutingProfile", - "aws:connect/securityProfile:SecurityProfile", - "aws:connect/user:User", - "aws:connect/userHierarchyGroup:UserHierarchyGroup", - "aws:connect/vocabulary:Vocabulary", - "aws:controltower/landingZone:LandingZone", - "aws:costexplorer/anomalyMonitor:AnomalyMonitor", - "aws:costexplorer/anomalySubscription:AnomalySubscription", - "aws:costexplorer/costCategory:CostCategory", - "aws:cur/reportDefinition:ReportDefinition", - "aws:customerprofiles/domain:Domain", - "aws:dataexchange/dataSet:DataSet", - "aws:dataexchange/revision:Revision", - "aws:datapipeline/pipeline:Pipeline", - "aws:datasync/agent:Agent", - "aws:datasync/efsLocation:EfsLocation", - "aws:datasync/fsxOpenZfsFileSystem:FsxOpenZfsFileSystem", - "aws:datasync/locationAzureBlob:LocationAzureBlob", - "aws:datasync/locationFsxLustre:LocationFsxLustre", - "aws:datasync/locationFsxOntapFileSystem:LocationFsxOntapFileSystem", - "aws:datasync/locationFsxWindows:LocationFsxWindows", - "aws:datasync/locationHdfs:LocationHdfs", - "aws:datasync/locationObjectStorage:LocationObjectStorage", - "aws:datasync/locationSmb:LocationSmb", - "aws:datasync/nfsLocation:NfsLocation", - "aws:datasync/s3Location:S3Location", - "aws:datasync/task:Task", - "aws:datazone/domain:Domain", - "aws:dax/cluster:Cluster", - "aws:detective/graph:Graph", - "aws:devicefarm/devicePool:DevicePool", - "aws:devicefarm/instanceProfile:InstanceProfile", - "aws:devicefarm/networkProfile:NetworkProfile", - "aws:devicefarm/project:Project", - "aws:devicefarm/testGridProject:TestGridProject", - "aws:devopsguru/resourceCollection:ResourceCollection", - "aws:directconnect/connection:Connection", - "aws:directconnect/hostedPrivateVirtualInterfaceAccepter:HostedPrivateVirtualInterfaceAccepter", - "aws:directconnect/hostedPublicVirtualInterfaceAccepter:HostedPublicVirtualInterfaceAccepter", - "aws:directconnect/hostedTransitVirtualInterfaceAcceptor:HostedTransitVirtualInterfaceAcceptor", - "aws:directconnect/linkAggregationGroup:LinkAggregationGroup", - "aws:directconnect/privateVirtualInterface:PrivateVirtualInterface", - "aws:directconnect/publicVirtualInterface:PublicVirtualInterface", - "aws:directconnect/transitVirtualInterface:TransitVirtualInterface", - "aws:directoryservice/directory:Directory", - "aws:directoryservice/serviceRegion:ServiceRegion", - "aws:dlm/lifecyclePolicy:LifecyclePolicy", - "aws:dms/certificate:Certificate", - "aws:dms/endpoint:Endpoint", - "aws:dms/eventSubscription:EventSubscription", - "aws:dms/replicationConfig:ReplicationConfig", - "aws:dms/replicationInstance:ReplicationInstance", - "aws:dms/replicationSubnetGroup:ReplicationSubnetGroup", - "aws:dms/replicationTask:ReplicationTask", - "aws:dms/s3Endpoint:S3Endpoint", - "aws:docdb/cluster:Cluster", - "aws:docdb/clusterInstance:ClusterInstance", - "aws:docdb/clusterParameterGroup:ClusterParameterGroup", - "aws:docdb/elasticCluster:ElasticCluster", - "aws:docdb/eventSubscription:EventSubscription", - "aws:docdb/subnetGroup:SubnetGroup", - "aws:drs/replicationConfigurationTemplate:ReplicationConfigurationTemplate", - "aws:dynamodb/table:Table", - "aws:dynamodb/tableReplica:TableReplica", - "aws:ebs/snapshot:Snapshot", - "aws:ebs/snapshotCopy:SnapshotCopy", - "aws:ebs/snapshotImport:SnapshotImport", - "aws:ebs/volume:Volume", - "aws:ec2/ami:Ami", - "aws:ec2/amiCopy:AmiCopy", - "aws:ec2/amiFromInstance:AmiFromInstance", - "aws:ec2/capacityBlockReservation:CapacityBlockReservation", - "aws:ec2/capacityReservation:CapacityReservation", - "aws:ec2/carrierGateway:CarrierGateway", - "aws:ec2/customerGateway:CustomerGateway", - "aws:ec2/dedicatedHost:DedicatedHost", - "aws:ec2/defaultNetworkAcl:DefaultNetworkAcl", - "aws:ec2/defaultRouteTable:DefaultRouteTable", - "aws:ec2/defaultSecurityGroup:DefaultSecurityGroup", - "aws:ec2/defaultSubnet:DefaultSubnet", - "aws:ec2/defaultVpc:DefaultVpc", - "aws:ec2/defaultVpcDhcpOptions:DefaultVpcDhcpOptions", - "aws:ec2/egressOnlyInternetGateway:EgressOnlyInternetGateway", - "aws:ec2/eip:Eip", - "aws:ec2/fleet:Fleet", - "aws:ec2/flowLog:FlowLog", - "aws:ec2/instance:Instance", - "aws:ec2/internetGateway:InternetGateway", - "aws:ec2/keyPair:KeyPair", - "aws:ec2/launchTemplate:LaunchTemplate", - "aws:ec2/localGatewayRouteTableVpcAssociation:LocalGatewayRouteTableVpcAssociation", - "aws:ec2/managedPrefixList:ManagedPrefixList", - "aws:ec2/natGateway:NatGateway", - "aws:ec2/networkAcl:NetworkAcl", - "aws:ec2/networkInsightsAnalysis:NetworkInsightsAnalysis", - "aws:ec2/networkInsightsPath:NetworkInsightsPath", - "aws:ec2/networkInterface:NetworkInterface", - "aws:ec2/placementGroup:PlacementGroup", - "aws:ec2/routeTable:RouteTable", - "aws:ec2/securityGroup:SecurityGroup", - "aws:ec2/spotFleetRequest:SpotFleetRequest", - "aws:ec2/spotInstanceRequest:SpotInstanceRequest", - "aws:ec2/subnet:Subnet", - "aws:ec2/trafficMirrorFilter:TrafficMirrorFilter", - "aws:ec2/trafficMirrorSession:TrafficMirrorSession", - "aws:ec2/trafficMirrorTarget:TrafficMirrorTarget", - "aws:ec2/vpc:Vpc", - "aws:ec2/vpcDhcpOptions:VpcDhcpOptions", - "aws:ec2/vpcEndpoint:VpcEndpoint", - "aws:ec2/vpcEndpointService:VpcEndpointService", - "aws:ec2/vpcIpam:VpcIpam", - "aws:ec2/vpcIpamPool:VpcIpamPool", - "aws:ec2/vpcIpamResourceDiscovery:VpcIpamResourceDiscovery", - "aws:ec2/vpcIpamResourceDiscoveryAssociation:VpcIpamResourceDiscoveryAssociation", - "aws:ec2/vpcIpamScope:VpcIpamScope", - "aws:ec2/vpcPeeringConnection:VpcPeeringConnection", - "aws:ec2/vpcPeeringConnectionAccepter:VpcPeeringConnectionAccepter", - "aws:ec2/vpnConnection:VpnConnection", - "aws:ec2/vpnGateway:VpnGateway", - "aws:ec2clientvpn/endpoint:Endpoint", - "aws:ec2transitgateway/connect:Connect", - "aws:ec2transitgateway/connectPeer:ConnectPeer", - "aws:ec2transitgateway/instanceConnectEndpoint:InstanceConnectEndpoint", - "aws:ec2transitgateway/multicastDomain:MulticastDomain", - "aws:ec2transitgateway/peeringAttachment:PeeringAttachment", - "aws:ec2transitgateway/peeringAttachmentAccepter:PeeringAttachmentAccepter", - "aws:ec2transitgateway/policyTable:PolicyTable", - "aws:ec2transitgateway/routeTable:RouteTable", - "aws:ec2transitgateway/transitGateway:TransitGateway", - "aws:ec2transitgateway/vpcAttachment:VpcAttachment", - "aws:ec2transitgateway/vpcAttachmentAccepter:VpcAttachmentAccepter", - "aws:ecr/repository:Repository", - "aws:ecrpublic/repository:Repository", - "aws:ecs/capacityProvider:CapacityProvider", - "aws:ecs/cluster:Cluster", - "aws:ecs/service:Service", - "aws:ecs/taskDefinition:TaskDefinition", - "aws:ecs/taskSet:TaskSet", - "aws:efs/accessPoint:AccessPoint", - "aws:efs/fileSystem:FileSystem", - "aws:eks/accessEntry:AccessEntry", - "aws:eks/addon:Addon", - "aws:eks/cluster:Cluster", - "aws:eks/fargateProfile:FargateProfile", - "aws:eks/identityProviderConfig:IdentityProviderConfig", - "aws:eks/nodeGroup:NodeGroup", - "aws:eks/podIdentityAssociation:PodIdentityAssociation", - "aws:elasticache/cluster:Cluster", - "aws:elasticache/parameterGroup:ParameterGroup", - "aws:elasticache/replicationGroup:ReplicationGroup", - "aws:elasticache/reservedCacheNode:ReservedCacheNode", - "aws:elasticache/serverlessCache:ServerlessCache", - "aws:elasticache/subnetGroup:SubnetGroup", - "aws:elasticache/user:User", - "aws:elasticache/userGroup:UserGroup", - "aws:elasticbeanstalk/application:Application", - "aws:elasticbeanstalk/applicationVersion:ApplicationVersion", - "aws:elasticbeanstalk/environment:Environment", - "aws:elasticsearch/domain:Domain", - "aws:elb/loadBalancer:LoadBalancer", - "aws:emr/cluster:Cluster", - "aws:emr/studio:Studio", - "aws:emrcontainers/jobTemplate:JobTemplate", - "aws:emrcontainers/virtualCluster:VirtualCluster", - "aws:emrserverless/application:Application", - "aws:evidently/feature:Feature", - "aws:evidently/launch:Launch", - "aws:evidently/project:Project", - "aws:evidently/segment:Segment", - "aws:finspace/kxCluster:KxCluster", - "aws:finspace/kxDatabase:KxDatabase", - "aws:finspace/kxDataview:KxDataview", - "aws:finspace/kxEnvironment:KxEnvironment", - "aws:finspace/kxScalingGroup:KxScalingGroup", - "aws:finspace/kxUser:KxUser", - "aws:finspace/kxVolume:KxVolume", - "aws:fis/experimentTemplate:ExperimentTemplate", - "aws:fms/policy:Policy", - "aws:fms/resourceSet:ResourceSet", - "aws:fsx/backup:Backup", - "aws:fsx/dataRepositoryAssociation:DataRepositoryAssociation", - "aws:fsx/fileCache:FileCache", - "aws:fsx/lustreFileSystem:LustreFileSystem", - "aws:fsx/ontapFileSystem:OntapFileSystem", - "aws:fsx/ontapStorageVirtualMachine:OntapStorageVirtualMachine", - "aws:fsx/ontapVolume:OntapVolume", - "aws:fsx/openZfsFileSystem:OpenZfsFileSystem", - "aws:fsx/openZfsSnapshot:OpenZfsSnapshot", - "aws:fsx/openZfsVolume:OpenZfsVolume", - "aws:fsx/windowsFileSystem:WindowsFileSystem", - "aws:gamelift/alias:Alias", - "aws:gamelift/build:Build", - "aws:gamelift/fleet:Fleet", - "aws:gamelift/gameServerGroup:GameServerGroup", - "aws:gamelift/gameSessionQueue:GameSessionQueue", - "aws:gamelift/matchmakingConfiguration:MatchmakingConfiguration", - "aws:gamelift/matchmakingRuleSet:MatchmakingRuleSet", - "aws:gamelift/script:Script", - "aws:glacier/vault:Vault", - "aws:globalaccelerator/accelerator:Accelerator", - "aws:globalaccelerator/crossAccountAttachment:CrossAccountAttachment", - "aws:globalaccelerator/customRoutingAccelerator:CustomRoutingAccelerator", - "aws:glue/catalogDatabase:CatalogDatabase", - "aws:glue/connection:Connection", - "aws:glue/crawler:Crawler", - "aws:glue/dataQualityRuleset:DataQualityRuleset", - "aws:glue/devEndpoint:DevEndpoint", - "aws:glue/job:Job", - "aws:glue/mLTransform:MLTransform", - "aws:glue/registry:Registry", - "aws:glue/schema:Schema", - "aws:glue/trigger:Trigger", - "aws:glue/workflow:Workflow", - "aws:grafana/workspace:Workspace", - "aws:guardduty/detector:Detector", - "aws:guardduty/filter:Filter", - "aws:guardduty/iPSet:IPSet", - "aws:guardduty/malwareProtectionPlan:MalwareProtectionPlan", - "aws:guardduty/threatIntelSet:ThreatIntelSet", - "aws:iam/instanceProfile:InstanceProfile", - "aws:iam/openIdConnectProvider:OpenIdConnectProvider", - "aws:iam/policy:Policy", - "aws:iam/role:Role", - "aws:iam/samlProvider:SamlProvider", - "aws:iam/serverCertificate:ServerCertificate", - "aws:iam/serviceLinkedRole:ServiceLinkedRole", - "aws:iam/user:User", - "aws:iam/virtualMfaDevice:VirtualMfaDevice", - "aws:imagebuilder/component:Component", - "aws:imagebuilder/containerRecipe:ContainerRecipe", - "aws:imagebuilder/distributionConfiguration:DistributionConfiguration", - "aws:imagebuilder/image:Image", - "aws:imagebuilder/imagePipeline:ImagePipeline", - "aws:imagebuilder/imageRecipe:ImageRecipe", - "aws:imagebuilder/infrastructureConfiguration:InfrastructureConfiguration", - "aws:imagebuilder/workflow:Workflow", - "aws:inspector/assessmentTemplate:AssessmentTemplate", - "aws:inspector/resourceGroup:ResourceGroup", - "aws:iot/authorizer:Authorizer", - "aws:iot/billingGroup:BillingGroup", - "aws:iot/caCertificate:CaCertificate", - "aws:iot/domainConfiguration:DomainConfiguration", - "aws:iot/policy:Policy", - "aws:iot/provisioningTemplate:ProvisioningTemplate", - "aws:iot/roleAlias:RoleAlias", - "aws:iot/thingGroup:ThingGroup", - "aws:iot/thingType:ThingType", - "aws:iot/topicRule:TopicRule", - "aws:ivs/channel:Channel", - "aws:ivs/playbackKeyPair:PlaybackKeyPair", - "aws:ivs/recordingConfiguration:RecordingConfiguration", - "aws:ivschat/loggingConfiguration:LoggingConfiguration", - "aws:ivschat/room:Room", - "aws:kendra/dataSource:DataSource", - "aws:kendra/faq:Faq", - "aws:kendra/index:Index", - "aws:kendra/querySuggestionsBlockList:QuerySuggestionsBlockList", - "aws:kendra/thesaurus:Thesaurus", - "aws:keyspaces/keyspace:Keyspace", - "aws:keyspaces/table:Table", - "aws:kinesis/analyticsApplication:AnalyticsApplication", - "aws:kinesis/firehoseDeliveryStream:FirehoseDeliveryStream", - "aws:kinesis/stream:Stream", - "aws:kinesis/videoStream:VideoStream", - "aws:kinesisanalyticsv2/application:Application", - "aws:kms/externalKey:ExternalKey", - "aws:kms/key:Key", - "aws:kms/replicaExternalKey:ReplicaExternalKey", - "aws:kms/replicaKey:ReplicaKey", - "aws:lambda/callbackFunction:CallbackFunction", - "aws:lambda/codeSigningConfig:CodeSigningConfig", - "aws:lambda/eventSourceMapping:EventSourceMapping", - "aws:lambda/function:Function", - "aws:lb/listener:Listener", - "aws:lb/listenerRule:ListenerRule", - "aws:lb/loadBalancer:LoadBalancer", - "aws:lb/targetGroup:TargetGroup", - "aws:lb/trustStore:TrustStore", - "aws:lex/v2modelsBot:V2modelsBot", - "aws:licensemanager/licenseConfiguration:LicenseConfiguration", - "aws:lightsail/bucket:Bucket", - "aws:lightsail/certificate:Certificate", - "aws:lightsail/containerService:ContainerService", - "aws:lightsail/database:Database", - "aws:lightsail/disk:Disk", - "aws:lightsail/distribution:Distribution", - "aws:lightsail/instance:Instance", - "aws:lightsail/keyPair:KeyPair", - "aws:lightsail/lb:Lb", - "aws:location/geofenceCollection:GeofenceCollection", - "aws:location/map:Map", - "aws:location/placeIndex:PlaceIndex", - "aws:location/routeCalculation:RouteCalculation", - "aws:location/tracker:Tracker", - "aws:m2/application:Application", - "aws:m2/environment:Environment", - "aws:macie/customDataIdentifier:CustomDataIdentifier", - "aws:macie/findingsFilter:FindingsFilter", - "aws:macie2/classificationJob:ClassificationJob", - "aws:macie2/member:Member", - "aws:mediaconvert/queue:Queue", - "aws:medialive/channel:Channel", - "aws:medialive/input:Input", - "aws:medialive/inputSecurityGroup:InputSecurityGroup", - "aws:medialive/multiplex:Multiplex", - "aws:mediapackage/channel:Channel", - "aws:mediastore/container:Container", - "aws:memorydb/acl:Acl", - "aws:memorydb/cluster:Cluster", - "aws:memorydb/parameterGroup:ParameterGroup", - "aws:memorydb/snapshot:Snapshot", - "aws:memorydb/subnetGroup:SubnetGroup", - "aws:memorydb/user:User", - "aws:mq/broker:Broker", - "aws:mq/configuration:Configuration", - "aws:msk/cluster:Cluster", - "aws:msk/replicator:Replicator", - "aws:msk/serverlessCluster:ServerlessCluster", - "aws:msk/vpcConnection:VpcConnection", - "aws:mskconnect/connector:Connector", - "aws:mskconnect/customPlugin:CustomPlugin", - "aws:mskconnect/workerConfiguration:WorkerConfiguration", - "aws:mwaa/environment:Environment", - "aws:neptune/cluster:Cluster", - "aws:neptune/clusterEndpoint:ClusterEndpoint", - "aws:neptune/clusterInstance:ClusterInstance", - "aws:neptune/clusterParameterGroup:ClusterParameterGroup", - "aws:neptune/eventSubscription:EventSubscription", - "aws:neptune/parameterGroup:ParameterGroup", - "aws:neptune/subnetGroup:SubnetGroup", - "aws:networkfirewall/firewall:Firewall", - "aws:networkfirewall/firewallPolicy:FirewallPolicy", - "aws:networkfirewall/ruleGroup:RuleGroup", - "aws:networkfirewall/tlsInspectionConfiguration:TlsInspectionConfiguration", - "aws:networkmanager/connectAttachment:ConnectAttachment", - "aws:networkmanager/connectPeer:ConnectPeer", - "aws:networkmanager/connection:Connection", - "aws:networkmanager/coreNetwork:CoreNetwork", - "aws:networkmanager/device:Device", - "aws:networkmanager/globalNetwork:GlobalNetwork", - "aws:networkmanager/link:Link", - "aws:networkmanager/site:Site", - "aws:networkmanager/siteToSiteVpnAttachment:SiteToSiteVpnAttachment", - "aws:networkmanager/transitGatewayPeering:TransitGatewayPeering", - "aws:networkmanager/transitGatewayRouteTableAttachment:TransitGatewayRouteTableAttachment", - "aws:networkmanager/vpcAttachment:VpcAttachment", - "aws:networkmonitor/monitor:Monitor", - "aws:networkmonitor/probe:Probe", - "aws:oam/link:Link", - "aws:oam/sink:Sink", - "aws:opensearch/domain:Domain", - "aws:opensearch/serverlessCollection:ServerlessCollection", - "aws:opensearchingest/pipeline:Pipeline", - "aws:opsworks/customLayer:CustomLayer", - "aws:opsworks/ecsClusterLayer:EcsClusterLayer", - "aws:opsworks/gangliaLayer:GangliaLayer", - "aws:opsworks/haproxyLayer:HaproxyLayer", - "aws:opsworks/javaAppLayer:JavaAppLayer", - "aws:opsworks/memcachedLayer:MemcachedLayer", - "aws:opsworks/mysqlLayer:MysqlLayer", - "aws:opsworks/nodejsAppLayer:NodejsAppLayer", - "aws:opsworks/phpAppLayer:PhpAppLayer", - "aws:opsworks/railsAppLayer:RailsAppLayer", - "aws:opsworks/stack:Stack", - "aws:opsworks/staticWebLayer:StaticWebLayer", - "aws:organizations/account:Account", - "aws:organizations/organizationalUnit:OrganizationalUnit", - "aws:organizations/policy:Policy", - "aws:organizations/resourcePolicy:ResourcePolicy", - "aws:paymentcryptography/key:Key", - "aws:pinpoint/app:App", - "aws:pinpoint/emailTemplate:EmailTemplate", - "aws:pinpoint/smsvoicev2OptOutList:Smsvoicev2OptOutList", - "aws:pinpoint/smsvoicev2PhoneNumber:Smsvoicev2PhoneNumber", - "aws:pipes/pipe:Pipe", - "aws:qldb/ledger:Ledger", - "aws:qldb/stream:Stream", - "aws:quicksight/analysis:Analysis", - "aws:quicksight/dashboard:Dashboard", - "aws:quicksight/dataSet:DataSet", - "aws:quicksight/dataSource:DataSource", - "aws:quicksight/folder:Folder", - "aws:quicksight/namespace:Namespace", - "aws:quicksight/template:Template", - "aws:quicksight/theme:Theme", - "aws:quicksight/vpcConnection:VpcConnection", - "aws:ram/resourceShare:ResourceShare", - "aws:rbin/rule:Rule", - "aws:rds/cluster:Cluster", - "aws:rds/clusterEndpoint:ClusterEndpoint", - "aws:rds/clusterInstance:ClusterInstance", - "aws:rds/clusterParameterGroup:ClusterParameterGroup", - "aws:rds/clusterSnapshot:ClusterSnapshot", - "aws:rds/customDbEngineVersion:CustomDbEngineVersion", - "aws:rds/eventSubscription:EventSubscription", - "aws:rds/instance:Instance", - "aws:rds/integration:Integration", - "aws:rds/optionGroup:OptionGroup", - "aws:rds/parameterGroup:ParameterGroup", - "aws:rds/proxy:Proxy", - "aws:rds/proxyEndpoint:ProxyEndpoint", - "aws:rds/reservedInstance:ReservedInstance", - "aws:rds/snapshot:Snapshot", - "aws:rds/snapshotCopy:SnapshotCopy", - "aws:rds/subnetGroup:SubnetGroup", - "aws:redshift/cluster:Cluster", - "aws:redshift/clusterSnapshot:ClusterSnapshot", - "aws:redshift/eventSubscription:EventSubscription", - "aws:redshift/hsmClientCertificate:HsmClientCertificate", - "aws:redshift/hsmConfiguration:HsmConfiguration", - "aws:redshift/parameterGroup:ParameterGroup", - "aws:redshift/snapshotCopyGrant:SnapshotCopyGrant", - "aws:redshift/snapshotSchedule:SnapshotSchedule", - "aws:redshift/subnetGroup:SubnetGroup", - "aws:redshift/usageLimit:UsageLimit", - "aws:redshiftserverless/namespace:Namespace", - "aws:redshiftserverless/workgroup:Workgroup", - "aws:rekognition/collection:Collection", - "aws:rekognition/streamProcessor:StreamProcessor", - "aws:resourceexplorer/index:Index", - "aws:resourceexplorer/view:View", - "aws:resourcegroups/group:Group", - "aws:rolesanywhere/profile:Profile", - "aws:rolesanywhere/trustAnchor:TrustAnchor", - "aws:route53/healthCheck:HealthCheck", - "aws:route53/resolverEndpoint:ResolverEndpoint", - "aws:route53/resolverFirewallDomainList:ResolverFirewallDomainList", - "aws:route53/resolverFirewallRuleGroup:ResolverFirewallRuleGroup", - "aws:route53/resolverFirewallRuleGroupAssociation:ResolverFirewallRuleGroupAssociation", - "aws:route53/resolverQueryLogConfig:ResolverQueryLogConfig", - "aws:route53/resolverRule:ResolverRule", - "aws:route53/zone:Zone", - "aws:route53domains/registeredDomain:RegisteredDomain", - "aws:route53recoveryreadiness/cell:Cell", - "aws:route53recoveryreadiness/readinessCheck:ReadinessCheck", - "aws:route53recoveryreadiness/recoveryGroup:RecoveryGroup", - "aws:route53recoveryreadiness/resourceSet:ResourceSet", - "aws:rum/appMonitor:AppMonitor", - "aws:s3/bucket:Bucket", - "aws:s3/bucketObject:BucketObject", - "aws:s3/bucketObjectv2:BucketObjectv2", - "aws:s3/bucketV2:BucketV2", - "aws:s3/objectCopy:ObjectCopy", - "aws:s3control/accessGrant:AccessGrant", - "aws:s3control/accessGrantsInstance:AccessGrantsInstance", - "aws:s3control/accessGrantsLocation:AccessGrantsLocation", - "aws:s3control/bucket:Bucket", - "aws:s3control/storageLensConfiguration:StorageLensConfiguration", - "aws:sagemaker/app:App", - "aws:sagemaker/appImageConfig:AppImageConfig", - "aws:sagemaker/codeRepository:CodeRepository", - "aws:sagemaker/dataQualityJobDefinition:DataQualityJobDefinition", - "aws:sagemaker/deviceFleet:DeviceFleet", - "aws:sagemaker/domain:Domain", - "aws:sagemaker/endpoint:Endpoint", - "aws:sagemaker/endpointConfiguration:EndpointConfiguration", - "aws:sagemaker/featureGroup:FeatureGroup", - "aws:sagemaker/flowDefinition:FlowDefinition", - "aws:sagemaker/humanTaskUI:HumanTaskUI", - "aws:sagemaker/image:Image", - "aws:sagemaker/model:Model", - "aws:sagemaker/modelPackageGroup:ModelPackageGroup", - "aws:sagemaker/monitoringSchedule:MonitoringSchedule", - "aws:sagemaker/notebookInstance:NotebookInstance", - "aws:sagemaker/pipeline:Pipeline", - "aws:sagemaker/project:Project", - "aws:sagemaker/space:Space", - "aws:sagemaker/studioLifecycleConfig:StudioLifecycleConfig", - "aws:sagemaker/userProfile:UserProfile", - "aws:sagemaker/workteam:Workteam", - "aws:scheduler/scheduleGroup:ScheduleGroup", - "aws:schemas/discoverer:Discoverer", - "aws:schemas/registry:Registry", - "aws:schemas/schema:Schema", - "aws:secretsmanager/secret:Secret", - "aws:securityhub/automationRule:AutomationRule", - "aws:securitylake/dataLake:DataLake", - "aws:securitylake/subscriber:Subscriber", - "aws:serverlessrepository/cloudFormationStack:CloudFormationStack", - "aws:servicecatalog/portfolio:Portfolio", - "aws:servicecatalog/product:Product", - "aws:servicecatalog/provisionedProduct:ProvisionedProduct", - "aws:servicediscovery/httpNamespace:HttpNamespace", - "aws:servicediscovery/privateDnsNamespace:PrivateDnsNamespace", - "aws:servicediscovery/publicDnsNamespace:PublicDnsNamespace", - "aws:servicediscovery/service:Service", - "aws:sesv2/configurationSet:ConfigurationSet", - "aws:sesv2/contactList:ContactList", - "aws:sesv2/dedicatedIpPool:DedicatedIpPool", - "aws:sesv2/emailIdentity:EmailIdentity", - "aws:sfn/activity:Activity", - "aws:sfn/stateMachine:StateMachine", - "aws:shield/protection:Protection", - "aws:shield/protectionGroup:ProtectionGroup", - "aws:signer/signingProfile:SigningProfile", - "aws:sns/topic:Topic", - "aws:sqs/queue:Queue", - "aws:ssm/activation:Activation", - "aws:ssm/association:Association", - "aws:ssm/contactsRotation:ContactsRotation", - "aws:ssm/document:Document", - "aws:ssm/maintenanceWindow:MaintenanceWindow", - "aws:ssm/parameter:Parameter", - "aws:ssm/patchBaseline:PatchBaseline", - "aws:ssmcontacts/contact:Contact", - "aws:ssmincidents/replicationSet:ReplicationSet", - "aws:ssmincidents/responsePlan:ResponsePlan", - "aws:ssoadmin/application:Application", - "aws:ssoadmin/permissionSet:PermissionSet", - "aws:ssoadmin/trustedTokenIssuer:TrustedTokenIssuer", - "aws:storagegateway/cachesIscsiVolume:CachesIscsiVolume", - "aws:storagegateway/fileSystemAssociation:FileSystemAssociation", - "aws:storagegateway/gateway:Gateway", - "aws:storagegateway/nfsFileShare:NfsFileShare", - "aws:storagegateway/smbFileShare:SmbFileShare", - "aws:storagegateway/storedIscsiVolume:StoredIscsiVolume", - "aws:storagegateway/tapePool:TapePool", - "aws:swf/domain:Domain", - "aws:synthetics/canary:Canary", - "aws:synthetics/group:Group", - "aws:timestreaminfluxdb/dbInstance:DbInstance", - "aws:timestreamwrite/database:Database", - "aws:timestreamwrite/table:Table", - "aws:transcribe/languageModel:LanguageModel", - "aws:transcribe/medicalVocabulary:MedicalVocabulary", - "aws:transcribe/vocabulary:Vocabulary", - "aws:transcribe/vocabularyFilter:VocabularyFilter", - "aws:transfer/agreement:Agreement", - "aws:transfer/certificate:Certificate", - "aws:transfer/connector:Connector", - "aws:transfer/profile:Profile", - "aws:transfer/server:Server", - "aws:transfer/user:User", - "aws:transfer/workflow:Workflow", - "aws:verifiedaccess/endpoint:Endpoint", - "aws:verifiedaccess/group:Group", - "aws:verifiedaccess/instance:Instance", - "aws:verifiedaccess/trustProvider:TrustProvider", - "aws:vpc/securityGroupEgressRule:SecurityGroupEgressRule", - "aws:vpc/securityGroupIngressRule:SecurityGroupIngressRule", - "aws:vpclattice/accessLogSubscription:AccessLogSubscription", - "aws:vpclattice/listener:Listener", - "aws:vpclattice/listenerRule:ListenerRule", - "aws:vpclattice/service:Service", - "aws:vpclattice/serviceNetwork:ServiceNetwork", - "aws:vpclattice/serviceNetworkServiceAssociation:ServiceNetworkServiceAssociation", - "aws:vpclattice/serviceNetworkVpcAssociation:ServiceNetworkVpcAssociation", - "aws:vpclattice/targetGroup:TargetGroup", - "aws:waf/rateBasedRule:RateBasedRule", - "aws:waf/rule:Rule", - "aws:waf/ruleGroup:RuleGroup", - "aws:waf/webAcl:WebAcl", - "aws:wafregional/rateBasedRule:RateBasedRule", - "aws:wafregional/rule:Rule", - "aws:wafregional/ruleGroup:RuleGroup", - "aws:wafregional/webAcl:WebAcl", - "aws:wafv2/ipSet:IpSet", - "aws:wafv2/regexPatternSet:RegexPatternSet", - "aws:wafv2/ruleGroup:RuleGroup", - "aws:wafv2/webAcl:WebAcl", - "aws:workspaces/connectionAlias:ConnectionAlias", - "aws:workspaces/directory:Directory", - "aws:workspaces/ipGroup:IpGroup", - "aws:workspaces/workspace:Workspace", - "aws:xray/group:Group", - "aws:xray/samplingRule:SamplingRule", -] diff --git a/modules/aws/provider/types.py b/modules/aws/provider/types.py deleted file mode 100644 index e69de29..0000000 diff --git a/modules/aws/resources.py b/modules/aws/resources.py deleted file mode 100644 index 9051e37..0000000 --- a/modules/aws/resources.py +++ /dev/null @@ -1,482 +0,0 @@ -# pulumi/modules/aws/resources.py - -""" -AWS Resource Management Module - -Handles creation and management of AWS resources including: -- S3 buckets and objects -- EC2 instances and volumes -- IAM roles and policies -- VPC and networking components -- Security groups and rules -""" - -from typing import Dict, List, Optional, Any, Tuple, TYPE_CHECKING -import pulumi -import pulumi_aws as aws -from pulumi import ResourceOptions, log - -from .types import AWSConfig, TenantAccountConfig -from .exceptions import ResourceCreationError -from .security import SecurityManager - -if TYPE_CHECKING: - from .provider import AWSProvider - from pulumi import Resource - -class ResourceManager: - """ - Manages AWS resources and operations. - - This class handles: - - Resource creation and configuration - - Resource tagging and metadata - - Resource protection settings - - Resource dependencies - """ - - def __init__(self, provider: 'AWSProvider'): - """ - Initialize Resource manager. - - Args: - provider: AWSProvider instance for resource management - """ - self.provider = provider - - def create_s3_bucket( - self, - name: str, - versioning: bool = True, - encryption: bool = True, - public_access_block: bool = True, - opts: Optional[ResourceOptions] = None - ) -> aws.s3.Bucket: - """ - Creates an S3 bucket with standard security configurations. - - Args: - name: Bucket name - versioning: Enable versioning - encryption: Enable encryption - public_access_block: Block public access - opts: Optional resource options - - Returns: - aws.s3.Bucket: Created S3 bucket - """ - if opts is None: - opts = ResourceOptions() - - # Create the bucket - bucket = aws.s3.Bucket( - name, - versioning=aws.s3.BucketVersioningArgs( - enabled=versioning - ) if versioning else None, - server_side_encryption_configuration=aws.s3.BucketServerSideEncryptionConfigurationArgs( - rule=aws.s3.BucketServerSideEncryptionConfigurationRuleArgs( - apply_server_side_encryption_by_default=aws.s3.BucketServerSideEncryptionConfigurationRuleApplyServerSideEncryptionByDefaultArgs( - sse_algorithm="AES256" - ) - ) - ) if encryption else None, - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - # Block public access if enabled - if public_access_block: - aws.s3.BucketPublicAccessBlock( - f"{name}-public-access-block", - bucket=bucket.id, - block_public_acls=True, - block_public_policy=True, - ignore_public_acls=True, - restrict_public_buckets=True, - opts=ResourceOptions( - provider=self.provider.provider, - parent=bucket, - protect=True - ) - ) - - return bucket - - def create_kms_key( - self, - name: str, - description: str, - deletion_window: int = 30, - enable_key_rotation: bool = True, - opts: Optional[ResourceOptions] = None - ) -> aws.kms.Key: - """ - Creates a KMS key with standard configuration. - - Args: - name: Key name - description: Key description - deletion_window: Key deletion window in days - enable_key_rotation: Enable automatic key rotation - opts: Optional resource options - - Returns: - aws.kms.Key: Created KMS key - """ - if opts is None: - opts = ResourceOptions() - - key = aws.kms.Key( - name, - description=description, - deletion_window_in_days=deletion_window, - enable_key_rotation=enable_key_rotation, - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - # Create an alias for the key - aws.kms.Alias( - f"{name}-alias", - name=f"alias/{name}", - target_key_id=key.id, - opts=ResourceOptions( - provider=self.provider.provider, - parent=key, - protect=True - ) - ) - - return key - - def create_cloudwatch_log_group( - self, - name: str, - retention_days: int = 30, - kms_key_id: Optional[str] = None, - opts: Optional[ResourceOptions] = None - ) -> aws.cloudwatch.LogGroup: - """ - Creates a CloudWatch Log Group. - - Args: - name: Log group name - retention_days: Log retention period - kms_key_id: Optional KMS key for encryption - opts: Optional resource options - - Returns: - aws.cloudwatch.LogGroup: Created log group - """ - if opts is None: - opts = ResourceOptions() - - return aws.cloudwatch.LogGroup( - name, - retention_in_days=retention_days, - kms_key_id=kms_key_id, - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - def create_sns_topic( - self, - name: str, - kms_master_key_id: Optional[str] = None, - opts: Optional[ResourceOptions] = None - ) -> aws.sns.Topic: - """ - Creates an SNS topic. - - Args: - name: Topic name - kms_master_key_id: Optional KMS key for encryption - opts: Optional resource options - - Returns: - aws.sns.Topic: Created SNS topic - """ - if opts is None: - opts = ResourceOptions() - - return aws.sns.Topic( - name, - kms_master_key_id=kms_master_key_id, - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - def create_sqs_queue( - self, - name: str, - visibility_timeout_seconds: int = 30, - message_retention_seconds: int = 345600, - kms_master_key_id: Optional[str] = None, - opts: Optional[ResourceOptions] = None - ) -> aws.sqs.Queue: - """ - Creates an SQS queue. - - Args: - name: Queue name - visibility_timeout_seconds: Message visibility timeout - message_retention_seconds: Message retention period - kms_master_key_id: Optional KMS key for encryption - opts: Optional resource options - - Returns: - aws.sqs.Queue: Created SQS queue - """ - if opts is None: - opts = ResourceOptions() - - return aws.sqs.Queue( - name, - visibility_timeout_seconds=visibility_timeout_seconds, - message_retention_seconds=message_retention_seconds, - kms_master_key_id=kms_master_key_id, - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - def create_dynamodb_table( - self, - name: str, - hash_key: str, - range_key: Optional[str] = None, - attributes: List[Dict[str, str]] = None, - billing_mode: str = "PAY_PER_REQUEST", - opts: Optional[ResourceOptions] = None - ) -> aws.dynamodb.Table: - """ - Creates a DynamoDB table. - - Args: - name: Table name - hash_key: Partition key name - range_key: Optional sort key name - attributes: List of attribute definitions - billing_mode: Billing mode (PAY_PER_REQUEST or PROVISIONED) - opts: Optional resource options - - Returns: - aws.dynamodb.Table: Created DynamoDB table - """ - if opts is None: - opts = ResourceOptions() - - if attributes is None: - attributes = [{"name": hash_key, "type": "S"}] - if range_key: - attributes.append({"name": range_key, "type": "S"}) - - return aws.dynamodb.Table( - name, - attributes=[ - aws.dynamodb.TableAttributeArgs(**attr) - for attr in attributes - ], - hash_key=hash_key, - range_key=range_key, - billing_mode=billing_mode, - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - def create_ecr_repository( - self, - name: str, - image_tag_mutability: str = "IMMUTABLE", - scan_on_push: bool = True, - opts: Optional[ResourceOptions] = None - ) -> aws.ecr.Repository: - """ - Creates an ECR repository. - - Args: - name: Repository name - image_tag_mutability: Tag mutability setting - scan_on_push: Enable image scanning on push - opts: Optional resource options - - Returns: - aws.ecr.Repository: Created ECR repository - """ - if opts is None: - opts = ResourceOptions() - - return aws.ecr.Repository( - name, - image_tag_mutability=image_tag_mutability, - image_scanning_configuration=aws.ecr.RepositoryImageScanningConfigurationArgs( - scan_on_push=scan_on_push - ), - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - def create_backup_vault( - self, - name: str, - kms_key_arn: Optional[str] = None, - opts: Optional[ResourceOptions] = None - ) -> aws.backup.Vault: - """ - Creates an AWS Backup vault. - - Args: - name: Vault name - kms_key_arn: Optional KMS key ARN for encryption - opts: Optional resource options - - Returns: - aws.backup.Vault: Created backup vault - """ - if opts is None: - opts = ResourceOptions() - - return aws.backup.Vault( - name, - kms_key_arn=kms_key_arn, - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - def create_backup_plan( - self, - name: str, - vault_name: str, - schedule: str, - retention_days: int = 30, - opts: Optional[ResourceOptions] = None - ) -> aws.backup.Plan: - """ - Creates an AWS Backup plan. - - Args: - name: Plan name - vault_name: Backup vault name - schedule: Backup schedule expression - retention_days: Backup retention period - opts: Optional resource options - - Returns: - aws.backup.Plan: Created backup plan - """ - if opts is None: - opts = ResourceOptions() - - backup_plan: aws.backup.Plan = aws.backup.Plan( - name, - rules=[aws.backup.PlanRuleArgs( - rule_name=f"{name}-rule", - target_vault_name=vault_name, - schedule=schedule, - lifecycle=aws.backup.PlanRuleLifecycleArgs( - delete_after=retention_days - ) - )], - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - return backup_plan - - def create_tenant_account( - self, - tenant_config: TenantAccountConfig, - parent_provider: aws.Provider, - organization_id: str, - depends_on: Optional[List[pulumi.Resource]] = None - ) -> aws.organizations.Account: - """Creates a new tenant account in the organization.""" - return aws.organizations.Account( - f"tenant-{tenant_config.name}", - name=tenant_config.name, - email=tenant_config.email, - role_name="OrganizationAccountAccessRole", - opts=ResourceOptions(provider=parent_provider, depends_on=depends_on) - ) - - def assume_role_in_tenant_account( - self, - account: aws.organizations.Account, - role_name: str, - region: str, - parent_provider: aws.Provider - ) -> aws.Provider: - """Creates a provider for the tenant account using assumed role.""" - return aws.Provider( - f"tenant-provider-{account.name}", - region=region, - assume_role=aws.ProviderAssumeRoleArgs( - role_arn=pulumi.Output.concat("arn:aws:iam::", account.id, f":role/{role_name}"), - session_name="TenantAccountAccess" - ), - opts=ResourceOptions(parent=account) - ) - - def deploy_tenant_resources( - self, - provider: aws.Provider, - account: aws.organizations.Account, - config: TenantAccountConfig - ) -> Dict[str, pulumi.Resource]: - """Deploys baseline resources in the tenant account.""" - # Implement tenant-specific resource deployment - return {} diff --git a/modules/aws/security.py b/modules/aws/security.py index e68aef0..83dac00 100644 --- a/modules/aws/security.py +++ b/modules/aws/security.py @@ -1,480 +1,19 @@ -# pulumi/modules/aws/security.py - -""" -AWS Security Management Module - -Handles creation and management of AWS security resources including: -- KMS keys and key policies -- Security group management -- WAF configurations -- Certificate management -- Secret management -- Security Hub enablement -""" - -from typing import Dict, List, Optional, Any, Union, TYPE_CHECKING -import json +# ./modules/aws/security.py +"""AWS Security Management Module""" +from typing import Dict, Any, Optional import pulumi import pulumi_aws as aws from pulumi import ResourceOptions, log -from .iam import IAMManager -from pulumi_aws import Provider -from pulumi_aws.cloudtrail import Trail -from .types import AWSConfig -if TYPE_CHECKING: - from .types import SecurityConfig - from .provider import AWSProvider +from .provider import AWSProvider class SecurityManager: - """ - Manages AWS security resources and operations. - - This class handles: - - KMS key management - - Security group configurations - - WAF and security rules - - Certificate and secret management - - Security Hub and GuardDuty - """ + """Manages AWS security resources and operations.""" def __init__(self, provider: 'AWSProvider'): - """ - Initialize Security manager. - - Args: - provider: AWSProvider instance for resource management - """ + """Initialize Security manager.""" self.provider = provider - def create_kms_key( - self, - name: str, - description: str, - key_usage: str = "ENCRYPT_DECRYPT", - deletion_window: int = 30, - enable_key_rotation: bool = True, - policy: Optional[Dict[str, Any]] = None, - opts: Optional[ResourceOptions] = None - ) -> aws.kms.Key: - """ - Creates a KMS key with specified configuration. - - Args: - name: Key name - description: Key description - key_usage: Key usage type - deletion_window: Key deletion window in days - enable_key_rotation: Enable automatic key rotation - policy: Key policy document - opts: Optional resource options - - Returns: - aws.kms.Key: Created KMS key - """ - if opts is None: - opts = ResourceOptions() - - # Create the KMS key - key = aws.kms.Key( - f"key-{name}", - description=description, - deletion_window_in_days=deletion_window, - enable_key_rotation=enable_key_rotation, - key_usage=key_usage, - policy=json.dumps(policy) if policy else None, - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - # Create an alias for the key - aws.kms.Alias( - f"alias-{name}", - name=f"alias/{name}", - target_key_id=key.id, - opts=ResourceOptions( - provider=self.provider.provider, - parent=key, - protect=True - ) - ) - - return key - - def create_certificate( - self, - domain_name: str, - validation_method: str = "DNS", - subject_alternative_names: Optional[List[str]] = None, - opts: Optional[ResourceOptions] = None - ) -> aws.acm.Certificate: - """ - Creates an ACM certificate. - - Args: - domain_name: Primary domain name - validation_method: Certificate validation method - subject_alternative_names: Additional domain names - opts: Optional resource options - - Returns: - aws.acm.Certificate: Created certificate - """ - if opts is None: - opts = ResourceOptions() - - certificate = aws.acm.Certificate( - f"cert-{domain_name}", - domain_name=domain_name, - validation_method=validation_method, - subject_alternative_names=subject_alternative_names, - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - return certificate - - def create_secret( - self, - name: str, - secret_string: Union[str, Dict[str, Any]], - description: Optional[str] = None, - kms_key_id: Optional[str] = None, - opts: Optional[ResourceOptions] = None - ) -> aws.secretsmanager.Secret: - """ - Creates a Secrets Manager secret. - - Args: - name: Secret name - secret_string: Secret value or dictionary - description: Secret description - kms_key_id: KMS key ID for encryption - opts: Optional resource options - - Returns: - aws.secretsmanager.Secret: Created secret - """ - if opts is None: - opts = ResourceOptions() - - # Create the secret - secret = aws.secretsmanager.Secret( - f"secret-{name}", - name=name, - description=description, - kms_key_id=kms_key_id, - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - # Create the secret version - secret_string_value = ( - json.dumps(secret_string) - if isinstance(secret_string, dict) - else secret_string - ) - - aws.secretsmanager.SecretVersion( - f"secret-version-{name}", - secret_id=secret.id, - secret_string=secret_string_value, - opts=ResourceOptions( - provider=self.provider.provider, - parent=secret, - protect=True - ) - ) - - return secret - - def enable_security_hub( - self, - enable_default_standards: bool = True, - control_findings_visible: bool = True, - opts: Optional[ResourceOptions] = None - ) -> aws.securityhub.Account: - """ - Enables AWS Security Hub for the account. - - Args: - enable_default_standards: Enable default security standards - control_findings_visible: Make findings visible - opts: Optional resource options - - Returns: - aws.securityhub.Account: Security Hub account configuration - """ - if opts is None: - opts = ResourceOptions() - - return aws.securityhub.Account( - "security-hub", - enable_default_standards=enable_default_standards, - control_findings_visible=control_findings_visible, - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - def enable_guardduty( - self, - enable_s3_logs: bool = True, - opts: Optional[ResourceOptions] = None - ) -> aws.guardduty.Detector: - """ - Enables Amazon GuardDuty for the account. - - Args: - enable_s3_logs: Enable S3 log monitoring - opts: Optional resource options - - Returns: - aws.guardduty.Detector: GuardDuty detector - """ - if opts is None: - opts = ResourceOptions() - - return aws.guardduty.Detector( - "guardduty-detector", - enable=True, - finding_publishing_frequency="ONE_HOUR", - datasources=aws.guardduty.DetectorDatasourcesArgs( - s3_logs=aws.guardduty.DetectorDatasourcesS3LogsArgs( - enable=enable_s3_logs - ) - ), - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - def create_waf_web_acl( - self, - name: str, - rules: List[Dict[str, Any]], - description: Optional[str] = None, - scope: str = "REGIONAL", - opts: Optional[ResourceOptions] = None - ) -> aws.wafv2.WebAcl: - """ - Creates a WAFv2 Web ACL. - - Args: - name: Web ACL name - rules: List of WAF rules - description: Web ACL description - scope: WAF scope (REGIONAL or CLOUDFRONT) - opts: Optional resource options - - Returns: - aws.wafv2.WebAcl: Created Web ACL - """ - if opts is None: - opts = ResourceOptions() - - return aws.wafv2.WebAcl( - f"waf-{name}", - name=name, - description=description, - scope=scope, - default_action=aws.wafv2.WebAclDefaultActionArgs( - allow=aws.wafv2.WebAclDefaultActionAllowArgs() - ), - rules=rules, - visibility_config=aws.wafv2.WebAclVisibilityConfigArgs( - cloudwatch_metrics_enabled=True, - metric_name=f"waf-{name}-metric", - sampled_requests_enabled=True - ), - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - def create_cloudwatch_log_group( - self, - name: str, - retention_days: int = 30, - kms_key_id: Optional[str] = None, - opts: Optional[ResourceOptions] = None - ) -> aws.cloudwatch.LogGroup: - """ - Creates a CloudWatch Log Group. - - Args: - name: Log group name - retention_days: Log retention period - kms_key_id: KMS key for encryption - opts: Optional resource options - - Returns: - aws.cloudwatch.LogGroup: Created log group - """ - if opts is None: - opts = ResourceOptions() - - return aws.cloudwatch.LogGroup( - f"log-group-{name}", - name=name, - retention_in_days=retention_days, - kms_key_id=kms_key_id, - tags=self.provider.get_tags(), - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - - def create_cloudtrail( - self, - name: str, - s3_bucket_name: str, - include_global_events: bool = True, - is_multi_region: bool = True, - kms_key_id: Optional[str] = None, - log_group_name: Optional[str] = None, - opts: Optional[ResourceOptions] = None - ) -> aws.cloudtrail.Trail: - """ - Creates a CloudTrail trail. - - Args: - name: Trail name - s3_bucket_name: S3 bucket for logs - include_global_events: Include global service events - is_multi_region: Enable multi-region trail - kms_key_id: KMS key for encryption - log_group_name: CloudWatch log group name - opts: Optional resource options - - Returns: - aws.cloudtrail.Trail: Created trail - """ - if opts is None: - opts = ResourceOptions() - - # Create CloudWatch log group if specified - log_group = None - role = None # Initialize role variable - if log_group_name: - log_group = self.create_cloudwatch_log_group( - log_group_name, - opts=ResourceOptions( - provider=self.provider.provider, - parent=opts.parent if opts else None - ) - ) - - # Create IAM role for CloudTrail to CloudWatch Logs - assume_role_policy = { - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Principal": { - "Service": "cloudtrail.amazonaws.com" - }, - "Action": "sts:AssumeRole" - }] - } - - role = aws.iam.Role( - f"cloudtrail-cloudwatch-role-{name}", - assume_role_policy=json.dumps(assume_role_policy), - tags=self.provider.get_tags(), - opts=ResourceOptions( - provider=self.provider.provider, - parent=log_group - ) - ) - - # Attach policy to role - aws.iam.RolePolicy( - f"cloudtrail-cloudwatch-policy-{name}", - role=role.id, - policy=pulumi.Output.all(log_group_arn=log_group.arn).apply( - lambda args: json.dumps({ - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Action": [ - "logs:CreateLogStream", - "logs:PutLogEvents" - ], - "Resource": f"{args['log_group_arn']}:*" - }] - }) - ), - opts=ResourceOptions( - provider=self.provider.provider, - parent=role - ) - ) - - # Create the trail - trail_args = { - "name": name, - "s3_bucket_name": s3_bucket_name, - "include_global_service_events": include_global_events, - "is_multi_region_trail": is_multi_region, - "kms_key_id": kms_key_id, - "tags": self.provider.get_tags() - } - - if log_group and role: # Add check for both resources - trail_args.update({ - "cloud_watch_logs_group_arn": pulumi.Output.concat(log_group.arn, ":*"), - "cloud_watch_logs_role_arn": role.arn - }) - - return aws.cloudtrail.Trail( - f"trail-{name}", - **trail_args, - opts=ResourceOptions.merge( - ResourceOptions( - provider=self.provider.provider, - protect=True - ), - opts - ) - ) - def deploy_security_controls(self) -> Dict[str, Any]: """Deploys security controls and returns outputs.""" try: @@ -507,58 +46,3 @@ def deploy_security_controls(self) -> Dict[str, Any]: except Exception as e: log.error(f"Failed to deploy security controls: {str(e)}") raise - -def setup_cloudtrail( - config: AWSConfig, - provider: Provider, - depends_on: Optional[List[pulumi.Resource]] = None -) -> Trail: - """ - Sets up AWS CloudTrail for audit logging. - - Args: - config: AWS configuration - provider: AWS provider - depends_on: Optional resource dependencies - - Returns: - aws.cloudtrail.Trail: Configured CloudTrail - - TODO: - - Enhance log encryption - - Add log validation - - Implement log analysis - - Add automated alerting - - Enhance retention policies - """ - try: - # Create S3 bucket for CloudTrail logs - trail_bucket = aws.s3.Bucket( - "cloudtrail-logs", - force_destroy=True, - opts=ResourceOptions( - provider=provider, - depends_on=depends_on, - protect=True - ) - ) - - # Create CloudTrail - trail = aws.cloudtrail.Trail( - "audit-trail", - s3_bucket_name=trail_bucket.id, - include_global_service_events=True, - is_multi_region_trail=True, - enable_logging=True, - opts=ResourceOptions( - provider=provider, - depends_on=[trail_bucket], - protect=True - ) - ) - - return trail - - except Exception as e: - pulumi.log.error(f"Error setting up CloudTrail: {str(e)}") - raise diff --git a/modules/aws/types.py b/modules/aws/types.py index 39ddf0c..eb8dc98 100644 --- a/modules/aws/types.py +++ b/modules/aws/types.py @@ -1,315 +1,75 @@ -# pulumi/modules/aws/types.py - -""" -AWS Module Configuration Types - -Defines data classes for AWS module configurations using Pydantic for type safety and validation. -Ensures integration of compliance configurations. - -Classes: -- IAMUserConfig: IAM user configuration. -- ControlTowerConfig: AWS Control Tower configuration. -- TenantAccountConfig: Tenant account configuration. -- GlobalTags: Global tags for resources. -- AWSConfig: Aggregated AWS configurations, including compliance settings. -- EksNodeGroupConfig: Configuration for EKS node groups. -- EksAddonConfig: Configuration for EKS add-ons. -- EksConfig: Configuration for EKS clusters. -""" +# ./modules/aws/types.py +"""AWS Module Configuration Types""" from __future__ import annotations -from typing import List, Dict, Optional, Any, Union, TypedDict, Tuple, TYPE_CHECKING, Protocol +from typing import List, Dict, Optional, Any, Union, TypedDict, Protocol from pydantic import BaseModel, Field, validator, root_validator -from core.types import ComplianceConfig -import pulumi -import pulumi_aws as aws -import ipaddress -from .organization import AWSOrganization -from .security import SecurityManager -from .networking import NetworkManager -from .resources import ResourceManager - -if TYPE_CHECKING: - from .security import SecurityManager - from .networking import NetworkManager - from .organization import AWSOrganization - from .resources import ResourceManager - from .iam import IAMManager - from .eks import EksManager - from .types import AWSConfig, AWSManagers - from .provider import AWSProvider +from ..core.types import ComplianceConfig class IAMUserConfig(BaseModel): """Configuration for an IAM User in AWS.""" - name: str = Field(..., description="Name of the IAM user.") email: str = Field(..., description="Email address of the IAM user.") - groups: List[str] = Field( - default_factory=list, description="IAM groups the user belongs to." - ) - policies: List[str] = Field( - default_factory=list, description="IAM policy ARNs attached to the user." - ) - tags: Dict[str, str] = Field( - default_factory=dict, description="Tags to apply to the IAM user." - ) - path: Optional[str] = Field(default="/", description="IAM user path.") - permissions_boundary: Optional[str] = Field( - None, description="ARN of the policy to set as permissions boundary." - ) - -class SecurityGroupRule(BaseModel): - """Configuration for a security group rule.""" - type: str = Field(..., description="Rule type (ingress/egress)") - protocol: str = Field(..., description="Network protocol") - from_port: int = Field(..., description="Starting port range") - to_port: int = Field(..., description="Ending port range") - cidr_blocks: Optional[List[str]] = Field(None, description="CIDR blocks") - security_group_id: Optional[str] = Field(None, description="Source/destination security group") - description: Optional[str] = Field(None, description="Rule description") + groups: List[str] = Field(default_factory=list) + policies: List[str] = Field(default_factory=list) + tags: Dict[str, str] = Field(default_factory=dict) + path: Optional[str] = Field(default="/") + permissions_boundary: Optional[str] = Field(None) class NetworkConfig(BaseModel): """Network configuration for AWS resources.""" - vpc_cidr: str = Field(..., description="VPC CIDR block") + vpc_cidr: str = Field(default="10.0.0.0/16", description="VPC CIDR block") subnet_cidrs: Dict[str, List[str]] = Field( - ..., description="Subnet CIDR blocks by type (public/private)" + default_factory=lambda: { + "public": ["10.0.1.0/24", "10.0.2.0/24"], + "private": ["10.0.3.0/24", "10.0.4.0/24"] + } + ) + availability_zones: List[str] = Field( + default_factory=lambda: ["us-east-1a", "us-east-1b"] ) - availability_zones: List[str] = Field(..., description="Availability zones to use") - enable_nat_gateway: bool = Field(True, description="Enable NAT Gateway") - enable_vpn_gateway: bool = Field(False, description="Enable VPN Gateway") - enable_flow_logs: bool = Field(True, description="Enable VPC Flow Logs") - tags: Dict[str, str] = Field(default_factory=dict, description="Network resource tags") + enable_nat_gateway: bool = Field(default=True) + enable_vpn_gateway: bool = Field(default=False) + enable_flow_logs: bool = Field(default=True) + tags: Dict[str, str] = Field(default_factory=dict) @validator("vpc_cidr") def validate_vpc_cidr(cls, v): try: - ipaddress.ip_network(v) + from ipaddress import ip_network + ip_network(v) + return v except ValueError: raise ValueError(f"Invalid VPC CIDR: {v}") - return v - -class ControlTowerConfig(BaseModel): - """Configuration for AWS Control Tower.""" - - enabled: bool = Field(default=False, description="Enable AWS Control Tower.") - organizational_unit_name: str = Field( - default="LandingZone", description="Name of the Organizational Unit." - ) - execution_role_name: str = Field( - default="AWSControlTowerExecution", description="Name of the execution role." - ) - execution_role_arn: Optional[str] = Field( - None, description="ARN of the execution role." - ) - admin_role_name: str = Field( - default="AWSControlTowerAdmin", description="Name of the admin role." - ) - admin_role_arn: Optional[str] = Field(None, description="ARN of the admin role.") - audit_role_name: str = Field( - default="AWSControlTowerAudit", description="Name of the audit role." - ) - audit_role_arn: Optional[str] = Field(None, description="ARN of the audit role.") - log_archive_bucket: Optional[str] = Field( - None, description="Name of the log archive bucket." - ) - - @validator("enabled", pre=True) - def validate_control_tower_fields(cls, v, values): - if v: - required_fields = ["execution_role_arn", "admin_role_arn"] - missing = [field for field in required_fields if not values.get(field)] - if missing: - raise ValueError( - f"Missing fields for Control Tower: {', '.join(missing)}" - ) - return v class SecurityConfig(BaseModel): """Security configuration for AWS resources.""" - enable_security_hub: bool = Field(True, description="Enable Security Hub") - enable_guard_duty: bool = Field(True, description="Enable GuardDuty") - enable_config: bool = Field(True, description="Enable AWS Config") - enable_cloudtrail: bool = Field(True, description="Enable CloudTrail") - kms_deletion_window: int = Field(30, description="KMS key deletion window in days") - enable_key_rotation: bool = Field(True, description="Enable KMS key rotation") - security_group_rules: List[SecurityGroupRule] = Field( - default_factory=list, - description="Security group rules" - ) - - @validator("security_group_rules") - def validate_security_rules(cls, v): - for rule in v: - if rule.type not in ["ingress", "egress"]: - raise ValueError(f"Invalid rule type: {rule.type}") - if not rule.cidr_blocks and not rule.security_group_id: - raise ValueError("Either CIDR blocks or security group ID required") - return v - -class TenantAccountConfig(BaseModel): - """Configuration for a Tenant Account within AWS.""" - - name: str = Field(..., description="Name of the tenant account.") - email: str = Field( - ..., description="Email address associated with the tenant account." - ) - administrators: List[str] = Field( - default_factory=list, description="Administrators of the tenant account." - ) - users: List[str] = Field( - default_factory=list, description="Users of the tenant account." - ) - features: List[str] = Field( - default_factory=list, description="Enabled features for the tenant account." - ) - network: Optional[NetworkConfig] = Field( - None, description="Network configuration for the tenant." - ) - security: Optional[SecurityConfig] = Field( - None, description="Security configuration for the tenant." - ) - aws: Dict[str, Any] = Field( - default_factory=dict, - description="AWS-specific configuration for the tenant account.", - ) - tags: Dict[str, str] = Field( - default_factory=dict, description="Tags for resources in the tenant account." - ) - -class GlobalTags(BaseModel): - """Global tags to apply to all AWS resources.""" - - project: str = Field(default="konductor", description="Project name.") - managed_by: str = Field( - default="NASA_SCIP_OPERATIONS", description="Managed by identifier." - ) - environment: str = Field( - default="production", description="Environment identifier." - ) - cost_center: Optional[str] = Field(None, description="Cost center identifier.") - data_classification: Optional[str] = Field( - None, description="Data classification level." - ) - -class BackupConfig(BaseModel): - """Configuration for AWS backup policies.""" - enabled: bool = Field(True, description="Enable AWS Backup") - retention_days: int = Field(30, description="Backup retention period") - schedule_expression: str = Field("cron(0 5 ? * * *)", description="Backup schedule") - copy_actions: Optional[List[Dict[str, Any]]] = Field( - None, description="Cross-region/account copy actions" - ) - - @validator("retention_days") - def validate_retention_days(cls, v): - if v < 1: - raise ValueError("Retention days must be positive") - return v - - @validator("schedule_expression") - def validate_schedule(cls, v): - if not v.startswith("cron(") or not v.endswith(")"): - raise ValueError("Invalid cron expression format") - return v - -class MonitoringConfig(BaseModel): - """Configuration for AWS monitoring.""" - enable_enhanced_monitoring: bool = Field(True, description="Enable enhanced monitoring") - metrics_collection_interval: int = Field(60, description="Metrics collection interval") - log_retention_days: int = Field(90, description="Log retention period") - alarm_notification_topic: Optional[str] = Field(None, description="SNS topic for alarms") - -class EksNodeGroupConfig(BaseModel): - """Configuration for EKS node groups.""" - name: str = Field(..., description="Node group name") - instance_type: str = Field(default="t3.medium", description="EC2 instance type") - desired_size: int = Field(default=2, description="Desired number of nodes") - min_size: int = Field(default=1, description="Minimum number of nodes") - max_size: int = Field(default=3, description="Maximum number of nodes") - disk_size: int = Field(default=50, description="Node disk size in GB") - ami_type: str = Field(default="AL2_x86_64", description="AMI type") - capacity_type: str = Field(default="ON_DEMAND", description="Capacity type (ON_DEMAND/SPOT)") - labels: Dict[str, str] = Field(default_factory=dict, description="Kubernetes labels") - taints: Optional[List[Dict[str, str]]] = Field(None, description="Kubernetes taints") - -class EksAddonConfig(BaseModel): - """Configuration for EKS add-ons.""" - vpc_cni: bool = Field(default=True, description="Enable AWS VPC CNI") - coredns: bool = Field(default=True, description="Enable CoreDNS") - kube_proxy: bool = Field(default=True, description="Enable kube-proxy") - aws_load_balancer_controller: bool = Field(default=True, description="Enable AWS Load Balancer Controller") - cluster_autoscaler: bool = Field(default=True, description="Enable Cluster Autoscaler") - metrics_server: bool = Field(default=True, description="Enable Metrics Server") - aws_for_fluent_bit: bool = Field(default=True, description="Enable AWS for Fluent Bit") - -class EksConfig(BaseModel): - """Configuration for EKS clusters.""" - enabled: bool = Field(default=False, description="Enable EKS deployment") - cluster_name: str = Field(..., description="EKS cluster name") - kubernetes_version: str = Field(default="1.26", description="Kubernetes version") - endpoint_private_access: bool = Field(default=True, description="Enable private endpoint") - endpoint_public_access: bool = Field(default=False, description="Enable public endpoint") - node_groups: List[EksNodeGroupConfig] = Field(default_factory=list, description="Node group configurations") - addons: EksAddonConfig = Field(default_factory=EksAddonConfig, description="Add-on configurations") - enable_irsa: bool = Field(default=True, description="Enable IAM Roles for Service Accounts") - enable_secrets_encryption: bool = Field(default=True, description="Enable secrets encryption") - enable_vpc_cni_prefix_delegation: bool = Field(default=True, description="Enable VPC CNI prefix delegation") - - @validator("kubernetes_version") - def validate_k8s_version(cls, v): - valid_versions = ["1.24", "1.25", "1.26", "1.27"] - if v not in valid_versions: - raise ValueError(f"Invalid Kubernetes version: {v}") - return v - - @validator("node_groups") - def validate_node_groups(cls, v): - if not v: - raise ValueError("At least one node group required") - return v + enable_security_hub: bool = Field(True) + enable_guard_duty: bool = Field(True) + enable_config: bool = Field(True) + enable_cloudtrail: bool = Field(True) + kms_deletion_window: int = Field(30) + enable_key_rotation: bool = Field(True) + +class AWSProviderConfig(TypedDict): + """AWS Provider configuration.""" + region: str + profile: Optional[str] + access_key_id: Optional[str] + secret_access_key: Optional[str] + tags: Dict[str, str] class AWSConfig(BaseModel): - """Aggregated configuration class for AWS module settings.""" - - enabled: bool = Field(default=True, description="Enable the AWS module.") - profile: str = Field(default="main", description="AWS CLI profile to use.") - region: str = Field(default="us-west-2", description="AWS region for deployment.") - account_id: str = Field(..., description="AWS account ID.") - bucket: str = Field(..., description="Name of the S3 bucket for state storage.") - control_tower: ControlTowerConfig = Field( - default_factory=ControlTowerConfig, - description="AWS Control Tower configuration.", - ) - iam_users: List[IAMUserConfig] = Field( - default_factory=list, description="IAM user configurations." - ) - landingzones: List[TenantAccountConfig] = Field( - default_factory=list, description="Tenant account configurations." - ) - network: NetworkConfig = Field( - ..., description="Network configuration." - ) - security: SecurityConfig = Field( - default_factory=SecurityConfig, - description="Security configuration." - ) - backup: BackupConfig = Field( - default_factory=BackupConfig, - description="Backup configuration." - ) - monitoring: MonitoringConfig = Field( - default_factory=MonitoringConfig, - description="Monitoring configuration." - ) - global_tags: GlobalTags = Field( - default_factory=GlobalTags, description="Global tags for all resources." - ) - compliance: ComplianceConfig = Field( - default_factory=ComplianceConfig, description="Compliance configuration." - ) - version: str = Field( - default="0.0.1", description="Version of the local AWS module." - ) + """AWS Module configuration.""" + enabled: bool = Field(default=True) + profile: Optional[str] = Field(default=None) + region: str = Field(default="us-west-2") + account_id: Optional[str] = Field(default=None) + bucket: Optional[str] = Field(default=None) + network: Optional[NetworkConfig] = Field(default_factory=NetworkConfig) + security: SecurityConfig = Field(default_factory=SecurityConfig) + compliance: ComplianceConfig = Field(default_factory=ComplianceConfig) + tags: Dict[str, str] = Field(default_factory=dict) @validator("region") def validate_region(cls, v): @@ -318,95 +78,16 @@ def validate_region(cls, v): raise ValueError(f"Invalid AWS region: {v}") return v - @root_validator - def validate_network_config(cls, values): - """Validate network configuration.""" - if "network" in values: - network = values["network"] - if len(network.availability_zones) < 2: - raise ValueError("At least 2 availability zones required") - return values - @classmethod def merge(cls, user_config: Dict[str, Any]) -> "AWSConfig": - """Merges user configuration with defaults, handling compliance integration.""" + """Merge user configuration with defaults.""" aws_specific_keys = {k for k in user_config.keys() if k != "compliance"} compliance_config = user_config.get("compliance", {}) aws_config = {k: user_config[k] for k in aws_specific_keys} - # Build compliance configuration + if "network" not in aws_config: + aws_config["network"] = NetworkConfig().model_dump() + compliance = ComplianceConfig.merge(compliance_config) aws_config["compliance"] = compliance - return cls(**aws_config) - -class ResourceManagerProtocol(Protocol): - def deploy_resources(self) -> dict[str, Any]: ... - -class AWSManagers(TypedDict): - organization: AWSOrganization - security: SecurityManager - networking: NetworkManager - resources: ResourceManagerProtocol - -def validate_config(config: AWSConfig) -> None: - """ - Validates the AWS configuration. - - Args: - config: AWS configuration to validate. - - Raises: - ValueError: If configuration is invalid. - """ - if not config.account_id: - raise ValueError("AWS account ID is required") - - if not config.region: - raise ValueError("AWS region is required") - - if config.control_tower.enabled: - if not config.control_tower.execution_role_arn: - raise ValueError("Control Tower execution role ARN is required when enabled") - - # Validate tenant configurations - if config.landingzones: - for tenant in config.landingzones: - if not tenant.email: - raise ValueError(f"Email is required for tenant account {tenant.name}") - - - -def validate_module_exports( - version: str, - resource: pulumi.Resource, - outputs: Dict[str, Any] -) -> bool: - """ - Validates module exports against required outputs. - - Args: - version: Module version string - resource: Main infrastructure resource - outputs: Dictionary of outputs to validate - - Returns: - bool: True if all required outputs are present - """ - required_outputs = { - "ops_data_bucket": "S3 bucket for operational data", - "organization": "AWS Organization ID", - "organization_arn": "AWS Organization ARN", - "vpc_id": "Primary VPC ID", - "subnet_ids": "List of subnet IDs", - "security_groups": "Map of security group IDs", - "kms_keys": "Map of KMS key ARNs", - "iam_roles": "Map of IAM role ARNs" - } - - missing = [key for key in required_outputs if key not in outputs] - if missing: - pulumi.log.warn(f"Missing required outputs: {', '.join(missing)}") - return False - - return True diff --git a/modules/core/__init__.py b/modules/core/__init__.py index 2bd91cd..db88625 100644 --- a/modules/core/__init__.py +++ b/modules/core/__init__.py @@ -7,11 +7,11 @@ and compliance controls. Key Components: -- Configuration Management -- Deployment Orchestration -- Resource Helpers -- Metadata Management -- Type Definitions +- Configuration Management: Handles configuration loading, validation, and merging +- Deployment Orchestration: Manages module deployment and dependencies +- Resource Management: Provides resource creation and transformation utilities +- Metadata Management: Handles global metadata and tagging +- Type Definitions: Defines core data structures and types Usage: from pulumi.core import ( @@ -140,11 +140,25 @@ ] def get_version() -> str: - """Returns the core module version.""" + """ + Returns the core module version. + + Returns: + str: The current version of the core module. + """ return __version__ def get_module_metadata() -> Dict[str, Any]: - """Returns metadata about the core module.""" + """ + Returns metadata about the core module. + + Returns: + Dict[str, Any]: A dictionary containing module metadata including: + - version: Current module version + - author: Module maintainers + - modules: List of available modules + - features: List of core features + """ return { "version": __version__, "author": __author__, diff --git a/modules/core/config.py b/modules/core/config.py index 9646401..47d2f6a 100644 --- a/modules/core/config.py +++ b/modules/core/config.py @@ -1,4 +1,4 @@ -# ../konductor/modules/core/config.py +# ./modules/core/config.py """ Configuration Management Module @@ -47,7 +47,7 @@ # Default module configuration DEFAULT_MODULE_CONFIG: Dict[str, ModuleDefaults] = { - "aws": {"enabled": False, "version": None, "config": {}}, + "aws": {"enabled": True, "version": None, "config": {}}, "cert_manager": {"enabled": False, "version": None, "config": {}}, "kubevirt": {"enabled": False, "version": None, "config": {}}, "multus": {"enabled": False, "version": None, "config": {}}, @@ -86,13 +86,13 @@ def get_module_config( Retrieves and prepares the configuration for a module. Args: - module_name: The name of the module to configure. - config: The Pulumi configuration object. - default_versions: A dictionary of default versions for modules. - namespace: Optional namespace for module configuration. + module_name: The name of the module to configure + config: The Pulumi configuration object + default_versions: A dictionary of default versions for modules + namespace: Optional namespace for module configuration Returns: - Tuple containing: + A tuple containing: - Module's configuration dictionary - Boolean indicating if the module is enabled @@ -106,12 +106,15 @@ def get_module_config( config={} )) + # Get module configuration from Pulumi config module_config: Dict[str, Any] = config.get_object(module_name) or {} + # Determine if module is enabled enabled = coerce_to_bool( module_config.get("enabled", module_defaults["enabled"]) ) + # Get module version version = module_config.get( "version", default_versions.get(module_name) @@ -510,7 +513,3 @@ def get_stack_outputs(init_config: InitializationConfig) -> StackOutputs: except Exception as e: log.error(f"Failed to generate stack outputs: {str(e)}") raise - - except Exception as e: - log.error(f"Failed to generate stack outputs: {str(e)}") - raise diff --git a/modules/core/deployment.py b/modules/core/deployment.py index ef4ac2d..480b500 100644 --- a/modules/core/deployment.py +++ b/modules/core/deployment.py @@ -1,57 +1,51 @@ -# ../konductor/modules/core/deployment.py +# ./modules/core/deployment.py import importlib from pulumi import log from typing import List, Dict, Optional from modules.core.types import InitializationConfig -from modules.core.interfaces import ModuleDeploymentResult +from modules.core.interfaces import ModuleDeploymentResult, ModuleInterface class DeploymentManager: - """ - Manages the deployment of modules based on the configuration. - """ + """Manages module deployment orchestration.""" def __init__(self, init_config: InitializationConfig): self.init_config = init_config - self.deployed_modules: Dict[str, ModuleDeploymentResult] = {} + self.modules = { + "aws": "modules.aws.deploy.AWSModule" + } + self.results: Dict[str, ModuleDeploymentResult] = {} def deploy_module(self, module_name: str) -> Optional[ModuleDeploymentResult]: - """ - Deploys a single module by dynamically importing and executing its deploy function. - - Args: - module_name: The name of the module to deploy. - - Returns: - ModuleDeploymentResult: The result of the deployment - """ try: - # Dynamically import the module's deploy function - deploy_module = importlib.import_module(f"modules.{module_name}.deploy") - deploy_func = getattr(deploy_module, "deploy", None) + # Load module + module = self._load_module(module_name) + + # Get module configuration + module_config = self.init_config.config.get_object(module_name) - if not callable(deploy_func): - raise AttributeError(f"Module {module_name} does not have a deploy function.") + # Let module validate its own config + validation_errors = module.validate_config(module_config) + if validation_errors: + raise ValueError(f"Configuration validation failed: {validation_errors}") - # Retrieve the module configuration - module_config = self.init_config.config.get_object(module_name) or {} + # Let module perform pre-deployment checks + pre_deploy_errors = module.pre_deploy_check() + if pre_deploy_errors: + raise ValueError(f"Pre-deployment checks failed: {pre_deploy_errors}") - # Call the deploy function with the necessary arguments - result = deploy_func( - config=module_config, - init_config=self.init_config - ) + # Deploy + result = module.deploy(module_config, self.init_config) - # Store the deployment result - self.deployed_modules[module_name] = result + # Let module validate deployment result + post_deploy_errors = module.post_deploy_validation(result) + if post_deploy_errors: + raise ValueError(f"Post-deployment validation failed: {post_deploy_errors}") return result - except ImportError as e: - log.error(f"Module {module_name} could not be imported: {str(e)}") - raise except Exception as e: - log.error(f"Error deploying module {module_name}: {str(e)}") + log.error(f"Module deployment failed: {str(e)}") raise def deploy_modules(self, modules_to_deploy: List[str]) -> None: @@ -61,12 +55,14 @@ def deploy_modules(self, modules_to_deploy: List[str]) -> None: Args: modules_to_deploy: A list of module names to deploy. """ + log.info(f"Deploying modules: {', '.join(modules_to_deploy)}") + # Build dependency graph dependency_graph = {} for module_name in modules_to_deploy: try: - module = importlib.import_module(f"modules.{module_name}.deploy") - dependencies = getattr(module, "get_dependencies", lambda: [])() + module = self._load_module(module_name) + dependencies = module.get_dependencies() dependency_graph[module_name] = dependencies except ImportError: log.warn(f"Could not load dependencies for module {module_name}") @@ -80,7 +76,7 @@ def deploy_with_deps(module: str): if module in deployed: return for dep in dependency_graph.get(module, []): - if dep in modules_to_deploy: # Only deploy dependencies that are in our list + if dep in modules_to_deploy: deploy_with_deps(dep) deployment_order.append(module) deployed.add(module) @@ -92,6 +88,23 @@ def deploy_with_deps(module: str): # Actually deploy the modules for module_name in deployment_order: try: - self.deploy_module(module_name) + result = self.deploy_module(module_name) + if result: + self.results[module_name] = result except Exception as e: log.error(f"Failed to deploy module {module_name}: {str(e)}") + raise + + def _load_module(self, module_name: str) -> ModuleInterface: + if module_name not in self.modules: + raise ValueError(f"Unknown module: {module_name}") + + module_path = self.modules[module_name] + module_parts = module_path.split(".") + + try: + module = importlib.import_module(".".join(module_parts[:-1])) + return getattr(module, module_parts[-1])() + except (ImportError, AttributeError) as e: + log.error(f"Failed to load module {module_name}: {str(e)}") + raise diff --git a/modules/core/git.py b/modules/core/git.py index 096ddb8..393587e 100644 --- a/modules/core/git.py +++ b/modules/core/git.py @@ -29,6 +29,9 @@ def get_latest_semver_tag(repo: Repo) -> Optional[str]: Returns: Optional[str]: Latest semantic version tag or None if no valid tags found. + + Raises: + GitCommandError: If there's an error accessing Git tags """ try: # Filter and sort tags that are valid semver @@ -140,23 +143,47 @@ def collect_git_info() -> GitInfo: Collects Git repository information. Returns: - GitInfo: An instance containing Git metadata. - """ - git_info = GitInfo() + GitInfo: An instance containing Git metadata including: + - commit_hash: Current commit hash + - branch_name: Current branch name + - remote_url: Repository remote URL + Raises: + subprocess.CalledProcessError: If Git commands fail + """ try: - git_info.commit_hash = subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode().strip() - except Exception as e: - log.warn(f"Failed to get commit hash: {str(e)}") + git_info = GitInfo() + + try: + git_info.commit_hash = subprocess.check_output( + ['git', 'rev-parse', 'HEAD'] + ).decode().strip() + except Exception as e: + log.warn(f"Failed to get commit hash: {str(e)}") + git_info.commit_hash = "unknown" + + try: + git_info.branch_name = subprocess.check_output( + ['git', 'rev-parse', '--abbrev-ref', 'HEAD'] + ).decode().strip() + except Exception as e: + log.warn(f"Failed to get branch name: {str(e)}") + git_info.branch_name = "unknown" + + try: + git_info.remote_url = subprocess.check_output( + ['git', 'config', '--get', 'remote.origin.url'] + ).decode().strip() + except Exception as e: + log.warn(f"Failed to get remote URL: {str(e)}") + git_info.remote_url = "unknown" + + return git_info - try: - git_info.branch_name = subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD']).decode().strip() except Exception as e: - log.warn(f"Failed to get branch name: {str(e)}") - - try: - git_info.remote_url = subprocess.check_output(['git', 'config', '--get', 'remote.origin.url']).decode().strip() - except Exception as e: - log.warn(f"Failed to get remote URL: {str(e)}") - - return git_info + log.error(f"Failed to collect Git information: {str(e)}") + return GitInfo( + commit_hash="unknown", + branch_name="unknown", + remote_url="unknown" + ) diff --git a/modules/core/initialization.py b/modules/core/initialization.py index 8b734ce..da6b2db 100644 --- a/modules/core/initialization.py +++ b/modules/core/initialization.py @@ -1,4 +1,13 @@ # ../konductor/modules/core/initialization.py + +""" +Pulumi Initialization Module + +This module handles the initialization of Pulumi projects and stacks. +It provides functionality for loading configurations, setting up workspaces, +and initializing the Pulumi runtime environment. +""" + import pulumi from pulumi import Config, get_stack, get_project, log @@ -9,8 +18,17 @@ def initialize_pulumi() -> InitializationConfig: """ Initializes Pulumi and loads the configuration. + This function performs the following: + 1. Loads Pulumi configuration + 2. Gets stack and project information + 3. Collects Git metadata + 4. Sets up initial metadata structure + Returns: InitializationConfig: The initialization configuration object. + + Raises: + Exception: If initialization fails """ try: # Load Pulumi configuration @@ -19,7 +37,7 @@ def initialize_pulumi() -> InitializationConfig: # Get stack and project names from Pulumi stack_name = get_stack() project_name = get_project() - log.info(f"DEBUG: project_name from get_project(): {project_name}") + log.info(f"Initializing Pulumi project: {project_name}, stack: {stack_name}") # Initialize default metadata structure metadata = { @@ -37,7 +55,7 @@ def initialize_pulumi() -> InitializationConfig: metadata=metadata ) - log.info(f"Initialized Pulumi with project: {project_name}, stack: {stack_name}") + log.info(f"Pulumi initialization completed successfully") return init_config except Exception as e: diff --git a/modules/core/interfaces.py b/modules/core/interfaces.py index f842963..2e3eed1 100644 --- a/modules/core/interfaces.py +++ b/modules/core/interfaces.py @@ -35,9 +35,11 @@ def get_config(self) -> Dict[str, Any]: ... def deploy(self) -> ModuleDeploymentResult: ... class ModuleInterface(Protocol): - """Protocol defining required module interface.""" + """Enhanced module interface with validation.""" def validate_config(self, config: Dict[str, Any]) -> List[str]: ... + def pre_deploy_check(self) -> List[str]: ... def deploy(self, ctx: DeploymentContext) -> ModuleDeploymentResult: ... + def post_deploy_validation(self, result: ModuleDeploymentResult) -> List[str]: ... def get_dependencies(self) -> List[str]: ... class ResourceManagerInterface(Protocol): diff --git a/modules/core/metadata.py b/modules/core/metadata.py index d24d6d3..bb1cb96 100644 --- a/modules/core/metadata.py +++ b/modules/core/metadata.py @@ -4,7 +4,8 @@ Metadata Management Module This module manages global metadata, labels, and annotations. -It includes functions to generate compliance and Git-related metadata. +It provides a thread-safe singleton for consistent metadata management +across all resources in a Pulumi program. """ import threading @@ -18,13 +19,27 @@ class MetadataSingleton: """ Thread-safe singleton class to manage global metadata. - Ensures consistent labels and annotations across all resources. + + This class ensures consistent labels and annotations across all resources. + It uses threading.Lock for thread safety and provides atomic operations + for metadata updates. + + Attributes: + _instance: The singleton instance + _lock: Thread lock for synchronization + _global_labels: Dictionary of global labels + _global_annotations: Dictionary of global annotations """ - _instance: ClassVar[Optional['MetadataSingleton']] = None - _lock: ClassVar[threading.Lock] = threading.Lock() + _instance: Optional['MetadataSingleton'] = None + _lock: ClassVar[Lock] = Lock() def __new__(cls) -> 'MetadataSingleton': - """Ensure only one instance is created.""" + """ + Ensure only one instance is created. + + Returns: + MetadataSingleton: The singleton instance + """ if cls._instance is None: with cls._lock: if cls._instance is None: @@ -42,23 +57,43 @@ def __init__(self) -> None: @property def global_labels(self) -> Dict[str, str]: - """Get global labels.""" + """ + Get global labels. + + Returns: + Dict[str, str]: Copy of global labels dictionary + """ with self._lock: return self._global_labels.copy() @property def global_annotations(self) -> Dict[str, str]: - """Get global annotations.""" + """ + Get global annotations. + + Returns: + Dict[str, str]: Copy of global annotations dictionary + """ with self._lock: return self._global_annotations.copy() def set_labels(self, labels: Dict[str, str]) -> None: - """Set global labels.""" + """ + Set global labels. + + Args: + labels: Dictionary of labels to set + """ with self._lock: self._global_labels.update(labels) def set_annotations(self, annotations: Dict[str, str]) -> None: - """Set global annotations.""" + """ + Set global annotations. + + Args: + annotations: Dictionary of annotations to set + """ with self._lock: self._global_annotations.update(annotations) diff --git a/modules/core/utils.py b/modules/core/utils.py index be78c1c..ce528c5 100644 --- a/modules/core/utils.py +++ b/modules/core/utils.py @@ -24,13 +24,15 @@ def set_resource_metadata( ) -> MetadataType: """ Updates resource metadata with global labels and annotations. - Handles both dict and ObjectMetaArgs metadata types. Args: metadata: Resource metadata to update global_labels: Global labels to apply global_annotations: Global annotations to apply + Returns: + MetadataType: Updated metadata + Raises: TypeError: If metadata is of an unsupported type """ @@ -59,6 +61,8 @@ def set_resource_metadata( log.error(f"Failed to update resource metadata: {str(e)}") raise + return metadata + def generate_global_transformations( global_labels: Dict[str, str], diff --git a/poetry.lock b/poetry.lock index db63d13..5a60047 100644 --- a/poetry.lock +++ b/poetry.lock @@ -853,8 +853,8 @@ astroid = ">=3.3.4,<=3.4.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, - {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, + {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, ] isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" mccabe = ">=0.6,<0.8"