AWSTemplateFormatVersion: '2010-09-09'
Description: >
  OpenBox platform — AWS infrastructure (reference template).
  Provisions: EKS cluster + node group, KMS CMK, S3 (OPA bundles),
  IRSA roles for openbox-backend + openbox-core, and (optionally)
  RDS Postgres + ElastiCache Redis + ECR repositories.

  Reference example — NOT a versioned module. Copy + adapt to your
  VPC layout, tag scheme, and security baseline.

# ------------------------------------------------------------
# Parameters
# ------------------------------------------------------------
Parameters:
  ClusterName:
    Type: String
    Default: openbox-prod
    Description: EKS cluster name (also used as resource-name prefix)

  ClusterVersion:
    Type: String
    Default: '1.31'
    Description: Kubernetes minor version

  VpcId:
    Type: AWS::EC2::VPC::Id
    Description: Existing VPC ID (bring your own)

  PrivateSubnetIds:
    Type: List<AWS::EC2::Subnet::Id>
    Description: >
      Private subnet IDs (≥ 2 across ≥ 2 AZs). EKS control plane + node group
      + optional RDS/ElastiCache all sit here.

  NodeInstanceType:
    Type: String
    Default: m6i.xlarge
    Description: EC2 instance type for the default node group

  NodeDesiredCapacity:
    Type: Number
    Default: 3
    MinValue: 1

  NodeMinCapacity:
    Type: Number
    Default: 2
    MinValue: 1

  NodeMaxCapacity:
    Type: Number
    Default: 8

  EnableRDS:
    Type: String
    Default: 'true'
    AllowedValues: ['true', 'false']
    Description: Provision RDS Postgres (Aurora-compatible); set false to skip

  EnableElastiCache:
    Type: String
    Default: 'true'
    AllowedValues: ['true', 'false']
    Description: Provision ElastiCache Redis; set false to skip

  EnableECR:
    Type: String
    Default: 'false'
    AllowedValues: ['true', 'false']
    Description: Create ECR repos for OpenBox images (private registry mirroring)

  # OpenBox convention — namespace and ServiceAccount names hard-coded to
  # match the Helm chart defaults. If you customize these in values.yaml,
  # update the IRSA principals below.
  OpenboxNamespace:
    Type: String
    Default: openbox

# ------------------------------------------------------------
# Conditions
# ------------------------------------------------------------
Conditions:
  DoRDS:         !Equals [!Ref EnableRDS, 'true']
  DoElastiCache: !Equals [!Ref EnableElastiCache, 'true']
  DoECR:         !Equals [!Ref EnableECR, 'true']

# ------------------------------------------------------------
# Resources
# ------------------------------------------------------------
Resources:

  # ==================================================
  # KMS — envelope encryption CMK
  # ==================================================
  OpenboxKmsKey:
    Type: AWS::KMS::Key
    Properties:
      Description: OpenBox envelope encryption CMK
      EnableKeyRotation: true
      PendingWindowInDays: 30
      KeyPolicy:
        Version: '2012-10-17'
        Statement:
          - Sid: EnableIAMUserPermissions
            Effect: Allow
            Principal: { AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root' }
            Action: 'kms:*'
            Resource: '*'
      Tags:
        - { Key: Name, Value: !Sub '${ClusterName}-envelope' }

  OpenboxKmsAlias:
    Type: AWS::KMS::Alias
    Properties:
      AliasName: !Sub 'alias/${ClusterName}-envelope'
      TargetKeyId: !Ref OpenboxKmsKey

  # ==================================================
  # S3 — OPA policy bundles bucket
  # ==================================================
  OpaBundlesBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Properties:
      BucketName: !Sub '${ClusterName}-opa-bundles-${AWS::AccountId}'
      VersioningConfiguration: { Status: Enabled }
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: aws:kms
              KMSMasterKeyID: !GetAtt OpenboxKmsKey.Arn

  # ==================================================
  # EKS — cluster role
  # ==================================================
  EksClusterRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${ClusterName}-cluster-role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal: { Service: eks.amazonaws.com }
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonEKSClusterPolicy

  # ==================================================
  # EKS — cluster
  # ==================================================
  EksCluster:
    Type: AWS::EKS::Cluster
    Properties:
      Name: !Ref ClusterName
      Version: !Ref ClusterVersion
      RoleArn: !GetAtt EksClusterRole.Arn
      ResourcesVpcConfig:
        SubnetIds: !Ref PrivateSubnetIds
        EndpointPrivateAccess: true
        EndpointPublicAccess: true

  # ==================================================
  # EKS — OIDC provider (required for IRSA)
  # Uses the community-maintained thumbprint for oidc.eks endpoints.
  # ==================================================
  EksOidcProvider:
    Type: AWS::IAM::OIDCProvider
    Properties:
      Url: !GetAtt EksCluster.OpenIdConnectIssuerUrl
      ClientIdList: [sts.amazonaws.com]
      ThumbprintList:
        - 9e99a48a9960b14926bb7f3b02e22da2b0ab7280

  # ==================================================
  # EKS — node group role + node group
  # ==================================================
  NodeGroupRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${ClusterName}-node-role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal: { Service: ec2.amazonaws.com }
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy
        - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
        - arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy

  EksNodeGroup:
    Type: AWS::EKS::Nodegroup
    Properties:
      ClusterName: !Ref EksCluster
      NodegroupName: default
      NodeRole: !GetAtt NodeGroupRole.Arn
      Subnets: !Ref PrivateSubnetIds
      InstanceTypes: [!Ref NodeInstanceType]
      ScalingConfig:
        DesiredSize: !Ref NodeDesiredCapacity
        MinSize:     !Ref NodeMinCapacity
        MaxSize:     !Ref NodeMaxCapacity

  # ==================================================
  # IRSA — openbox-backend service account
  # Trust policy is built as a JSON string via Fn::Sub since intrinsic
  # functions cannot be used as map keys in CloudFormation.
  # ==================================================
  BackendIrsaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: openbox-backend-irsa
      AssumeRolePolicyDocument: !Sub
        - |
          {
            "Version": "2012-10-17",
            "Statement": [{
              "Effect": "Allow",
              "Principal": {"Federated": "${OidcProviderArn}"},
              "Action": "sts:AssumeRoleWithWebIdentity",
              "Condition": {
                "StringEquals": {
                  "${OidcSub}:sub": "system:serviceaccount:${Ns}:openbox-backend",
                  "${OidcSub}:aud": "sts.amazonaws.com"
                }
              }
            }]
          }
        - OidcProviderArn: !Ref EksOidcProvider
          OidcSub: !Select [1, !Split ['https://', !GetAtt EksCluster.OpenIdConnectIssuerUrl]]
          Ns: !Ref OpenboxNamespace
      Policies:
        - PolicyName: openbox-backend-permissions
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action: [kms:Encrypt, kms:Decrypt, kms:GenerateDataKey, kms:DescribeKey]
                Resource: !GetAtt OpenboxKmsKey.Arn
              - Effect: Allow
                Action: [s3:GetObject, s3:PutObject, s3:DeleteObject, s3:ListBucket]
                Resource:
                  - !GetAtt OpaBundlesBucket.Arn
                  - !Sub '${OpaBundlesBucket.Arn}/*'

  # ==================================================
  # IRSA — openbox-core service account (KMS only)
  # ==================================================
  CoreIrsaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: openbox-core-irsa
      AssumeRolePolicyDocument: !Sub
        - |
          {
            "Version": "2012-10-17",
            "Statement": [{
              "Effect": "Allow",
              "Principal": {"Federated": "${OidcProviderArn}"},
              "Action": "sts:AssumeRoleWithWebIdentity",
              "Condition": {
                "StringEquals": {
                  "${OidcSub}:sub": "system:serviceaccount:${Ns}:openbox-core",
                  "${OidcSub}:aud": "sts.amazonaws.com"
                }
              }
            }]
          }
        - OidcProviderArn: !Ref EksOidcProvider
          OidcSub: !Select [1, !Split ['https://', !GetAtt EksCluster.OpenIdConnectIssuerUrl]]
          Ns: !Ref OpenboxNamespace
      Policies:
        - PolicyName: openbox-core-permissions
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action: [kms:Encrypt, kms:Decrypt, kms:GenerateDataKey, kms:DescribeKey]
                Resource: !GetAtt OpenboxKmsKey.Arn

  # ==================================================
  # RDS Postgres (OPTIONAL — EnableRDS=true)
  # ==================================================
  RdsSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Condition: DoRDS
    Properties:
      DBSubnetGroupDescription: OpenBox RDS subnet group
      SubnetIds: !Ref PrivateSubnetIds

  RdsSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Condition: DoRDS
    Properties:
      GroupDescription: OpenBox RDS ingress
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 5432
          ToPort: 5432
          CidrIp: 10.0.0.0/16          # tighten to EKS node SG in your version

  RdsCredentialsSecret:
    Type: AWS::SecretsManager::Secret
    Condition: DoRDS
    Properties:
      Name: openbox/rds-master
      Description: OpenBox RDS master credentials
      GenerateSecretString:
        SecretStringTemplate: '{"username": "openbox_admin"}'
        GenerateStringKey: password
        PasswordLength: 32
        ExcludeCharacters: '"@/\'

  RdsInstance:
    Type: AWS::RDS::DBInstance
    Condition: DoRDS
    DeletionPolicy: Snapshot
    UpdateReplacePolicy: Snapshot
    Properties:
      DBInstanceIdentifier: !Sub '${ClusterName}-openbox'
      Engine: postgres
      # Pin to a supported major/minor. Bump alongside AWS RDS deprecation
      # cadence — verify with: aws rds describe-db-engine-versions --engine postgres
      EngineVersion: '17.5'
      DBInstanceClass: db.r6i.large
      AllocatedStorage: '100'
      StorageType: gp3
      StorageEncrypted: true
      KmsKeyId: !GetAtt OpenboxKmsKey.Arn
      DBName: openbox
      MasterUsername: !Sub '{{resolve:secretsmanager:${RdsCredentialsSecret}:SecretString:username}}'
      MasterUserPassword: !Sub '{{resolve:secretsmanager:${RdsCredentialsSecret}:SecretString:password}}'
      DBSubnetGroupName: !Ref RdsSubnetGroup
      VPCSecurityGroups: [!Ref RdsSecurityGroup]
      BackupRetentionPeriod: 7
      DeletionProtection: true

  # ==================================================
  # ElastiCache Redis (OPTIONAL — EnableElastiCache=true)
  # ==================================================
  RedisSubnetGroup:
    Type: AWS::ElastiCache::SubnetGroup
    Condition: DoElastiCache
    Properties:
      Description: OpenBox Redis subnet group
      SubnetIds: !Ref PrivateSubnetIds

  RedisSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Condition: DoElastiCache
    Properties:
      GroupDescription: OpenBox Redis ingress
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 6379
          ToPort: 6379
          CidrIp: 10.0.0.0/16

  RedisCluster:
    Type: AWS::ElastiCache::CacheCluster
    Condition: DoElastiCache
    Properties:
      ClusterName: !Sub '${ClusterName}-redis'
      Engine: redis
      EngineVersion: '7.1'
      CacheNodeType: cache.t4g.small
      NumCacheNodes: 1
      CacheSubnetGroupName: !Ref RedisSubnetGroup
      VpcSecurityGroupIds: [!Ref RedisSecurityGroup]
      Port: 6379

  # ==================================================
  # ECR (OPTIONAL — EnableECR=true)
  # Creates one repo per OpenBox service image.
  # ==================================================
  EcrBackend:
    Type: AWS::ECR::Repository
    Condition: DoECR
    Properties:
      RepositoryName: openbox-backend
      ImageTagMutability: IMMUTABLE
      ImageScanningConfiguration: { ScanOnPush: true }

  EcrCore:
    Type: AWS::ECR::Repository
    Condition: DoECR
    Properties: { RepositoryName: openbox-core, ImageTagMutability: IMMUTABLE, ImageScanningConfiguration: { ScanOnPush: true } }

  EcrFe:
    Type: AWS::ECR::Repository
    Condition: DoECR
    Properties: { RepositoryName: openbox-fe, ImageTagMutability: IMMUTABLE, ImageScanningConfiguration: { ScanOnPush: true } }

  EcrGuardrails:
    Type: AWS::ECR::Repository
    Condition: DoECR
    Properties: { RepositoryName: guardrails-api, ImageTagMutability: IMMUTABLE, ImageScanningConfiguration: { ScanOnPush: true } }

  EcrModelHostPii:
    Type: AWS::ECR::Repository
    Condition: DoECR
    Properties: { RepositoryName: model-host-detect-pii, ImageTagMutability: IMMUTABLE, ImageScanningConfiguration: { ScanOnPush: true } }

  EcrModelHostNsfw:
    Type: AWS::ECR::Repository
    Condition: DoECR
    Properties: { RepositoryName: model-host-nsfw, ImageTagMutability: IMMUTABLE, ImageScanningConfiguration: { ScanOnPush: true } }

  EcrModelHostToxicity:
    Type: AWS::ECR::Repository
    Condition: DoECR
    Properties: { RepositoryName: model-host-toxicity, ImageTagMutability: IMMUTABLE, ImageScanningConfiguration: { ScanOnPush: true } }

# ------------------------------------------------------------
# Outputs — feed these into your Helm values-prod.yaml
# ------------------------------------------------------------
Outputs:
  ClusterName:
    Value: !Ref EksCluster
    Description: EKS cluster name (for aws eks update-kubeconfig)
  ClusterEndpoint:
    Value: !GetAtt EksCluster.Endpoint
  ClusterCaData:
    Value: !GetAtt EksCluster.CertificateAuthorityData
  OidcProviderArn:
    Value: !Ref EksOidcProvider
  Region:
    Value: !Ref AWS::Region

  KmsKeyArn:
    Value: !GetAtt OpenboxKmsKey.Arn
    Description: KMS CMK ARN — set values-prod.yaml openbox-backend.env.KMS_KEY_ARN

  OpaBundlesBucket:
    Value: !Ref OpaBundlesBucket
    Description: S3 bucket name — set values-prod.yaml openbox-backend.env.OPA_BUNDLE_BUCKET

  BackendIrsaRoleArn:
    Value: !GetAtt BackendIrsaRole.Arn
    Description: IRSA role ARN — annotate openbox-backend ServiceAccount

  CoreIrsaRoleArn:
    Value: !GetAtt CoreIrsaRole.Arn
    Description: IRSA role ARN — annotate openbox-core ServiceAccount

  RdsEndpoint:
    Condition: DoRDS
    Value: !GetAtt RdsInstance.Endpoint.Address
    Description: RDS endpoint (nil if EnableRDS=false)

  RdsSecretArn:
    Condition: DoRDS
    Value: !Ref RdsCredentialsSecret
    Description: Secrets Manager ARN holding {username,password}

  ElastiCacheEndpoint:
    Condition: DoElastiCache
    Value: !GetAtt RedisCluster.RedisEndpoint.Address
    Description: Redis endpoint (nil if EnableElastiCache=false)
