AWSTemplateFormatVersion: "2010-09-09"
Description: Deploy Windmill on EKS with Helm

Parameters:
  NodeInstanceType:
    Type: String
    Default: t3.medium
    AllowedValues:
      - t3.small
      - t3.medium
      - t3.large
      - t3.xlarge
      - t3.2xlarge
      - m5.large
      - m5.xlarge
      - m5.2xlarge
      - m5.4xlarge
      - c5.large
      - c5.xlarge
      - c5.2xlarge
      - r5.large
      - r5.xlarge
      - r5.2xlarge
    Description: EC2 instance type for the EKS worker nodes
  NodeGroupSize:
    Type: Number
    Default: 2
  RdsInstanceClass:
    Type: String
    Default: db.t3.small
    AllowedValues:
      - db.t3.micro
      - db.t3.small
      - db.t3.medium
      - db.t3.large
      - db.t3.xlarge
      - db.m5.large
      - db.m5.xlarge
      - db.m5.2xlarge
      - db.r5.large
      - db.r5.xlarge
      - db.r5.2xlarge
    Description: RDS instance class for the PostgreSQL database
  DBPassword:
    Type: String
    NoEcho: true
  WorkerReplicas:
    Type: Number
    Default: 2
  NativeWorkerReplicas:
    Type: Number
    Default: 1
  Enterprise:
    Type: String
    Default: false
    AllowedValues:
      - true
      - false
    Description: Enable Windmill Enterprise features (requires license key)

Mappings:
  RegionMap:
    us-east-1:
      AMI: ami-0cff7528ff583bf9a
    us-east-2:
      AMI: ami-0cd3c7f72edd5b06d
    us-west-1:
      AMI: ami-0d9858aa3c6322f73
    us-west-2:
      AMI: ami-098e42ae54c764c35
    ca-central-1:
      AMI: ami-00f881f027a6d74a0
    eu-west-1:
      AMI: ami-04dd4500af104442f
    eu-west-2:
      AMI: ami-0eb260c4d5475b901
    eu-west-3:
      AMI: ami-05e8e20cef0eaa9d0
    eu-central-1:
      AMI: ami-0bad4a5e987bdebde
    ap-northeast-1:
      AMI: ami-0b7546e839d7ace12
    ap-northeast-2:
      AMI: ami-0fd0765afb77bcca7
    ap-southeast-1:
      AMI: ami-0c802847a7dd848c0
    ap-southeast-2:
      AMI: ami-07620139298af599e
    ap-south-1:
      AMI: ami-0851b76e8b1bce90b
    sa-east-1:
      AMI: ami-054a31f1b3bf90920

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub "${AWS::StackName}-vpc"
        - Key: !Sub "kubernetes.io/cluster/${AWS::StackName}-cluster"
          Value: shared

  InternetGateway:
    Type: AWS::EC2::InternetGateway

  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: !Select [0, !GetAZs ""]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: kubernetes.io/role/elb
          Value: "1"
        - Key: !Sub "kubernetes.io/cluster/${AWS::StackName}-cluster"
          Value: shared
        - Key: Name
          Value: !Sub ${AWS::StackName}-public-subnet-1

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.2.0/24
      AvailabilityZone: !Select [1, !GetAZs ""]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: kubernetes.io/role/elb
          Value: "1"
        - Key: !Sub "kubernetes.io/cluster/${AWS::StackName}-cluster"
          Value: shared
        - Key: Name
          Value: !Sub ${AWS::StackName}-public-subnet-2

  RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC

  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: AttachGateway
    Properties:
      RouteTableId: !Ref RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  SubnetRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    DependsOn: PublicRoute
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref RouteTable

  SubnetRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    DependsOn: PublicRoute
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref RouteTable

  EKSClusterRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - eks.amazonaws.com
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonEKSClusterPolicy
        - arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy
      Policies:
        - PolicyName: EKSAccess
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - eks:*
                  - ec2:DescribeInstances
                  - ec2:DescribeRouteTables
                  - ec2:DescribeSecurityGroups
                  - ec2:DescribeSubnets
                  - ec2:DescribeVpcs
                  - iam:GetRole
                  - iam:ListRoles
                Resource: "*"
              - Effect: Allow
                Action:
                  - ssm:GetParameter
                  - ssm:PutParameter
                Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AWS::StackName}/*"
        - PolicyName: KubernetesAccess
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - eks:DescribeCluster
                  - eks:ListClusters
                  - eks:AccessKubernetesApi
                Resource: !Sub "arn:aws:eks:${AWS::Region}:${AWS::AccountId}:cluster/${AWS::StackName}-cluster"

  EKSNodeRole:
    Type: AWS::IAM::Role
    Properties:
      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

  EKSCluster:
    Type: AWS::EKS::Cluster
    Properties:
      Name: !Sub "${AWS::StackName}-cluster"
      RoleArn: !GetAtt EKSClusterRole.Arn
      ResourcesVpcConfig:
        SubnetIds:
          - !Ref PublicSubnet1
          - !Ref PublicSubnet2
        EndpointPublicAccess: true
      AccessConfig:
        AuthenticationMode: API_AND_CONFIG_MAP
        BootstrapClusterCreatorAdminPermissions: true
    DependsOn:
      - PublicRoute
      - VPCCleanup

  EKSClusterAccess:
    Type: AWS::EKS::AccessEntry
    Properties:
      ClusterName: !Ref EKSCluster
      PrincipalArn: !GetAtt EKSClusterRole.Arn
      Type: STANDARD
      Username: admin
      AccessPolicies:
        - PolicyArn: arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy
          AccessScope:
            Type: cluster

  EKSNodeGroup:
    Type: AWS::EKS::Nodegroup
    DependsOn:
      - WindmillDB
      - EKSCluster
    Properties:
      ClusterName: !Ref EKSCluster
      NodeRole: !GetAtt EKSNodeRole.Arn
      Subnets:
        - !Ref PublicSubnet1
        - !Ref PublicSubnet2
      ScalingConfig:
        MinSize: 1
        DesiredSize: !Ref NodeGroupSize
        MaxSize: 4
      InstanceTypes:
        - !Ref NodeInstanceType

  WindmillDBSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: Subnet group for RDS instance
      SubnetIds:
        - !Ref PublicSubnet1
        - !Ref PublicSubnet2

  WindmillDBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow PostgreSQL access from EKS nodes
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 5432
          ToPort: 5432
          CidrIp: 10.0.0.0/16

  WindmillDB:
    Type: AWS::RDS::DBInstance
    Properties:
      DBInstanceIdentifier: !Sub "${AWS::StackName}-db"
      AllocatedStorage: 20
      DBInstanceClass: !Ref RdsInstanceClass
      Engine: postgres
      EngineVersion: 17.2
      MasterUsername: postgres
      MasterUserPassword: !Ref DBPassword
      DBName: windmill
      PubliclyAccessible: false
      DBSubnetGroupName: !Ref WindmillDBSubnetGroup
      VPCSecurityGroups:
        - !Ref WindmillDBSecurityGroup
    DependsOn:
      - WindmillDBSubnetGroup

  WindmillInstallerInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref EKSClusterRole

  WindmillInstallerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for Windmill installer instance
      VpcId: !Ref VPC
      SecurityGroupEgress:
        - IpProtocol: -1
          FromPort: -1
          ToPort: -1
          CidrIp: 0.0.0.0/0

  WindmillInstaller:
    Type: AWS::EC2::Instance
    CreationPolicy:
      ResourceSignal:
        Timeout: PT30M # Gives 30 minutes for the installation to complete
    DependsOn:
      - EKSNodeGroup
      - WindmillDB
    Properties:
      ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", AMI]
      InstanceType: t3.micro
      IamInstanceProfile: !Ref WindmillInstallerInstanceProfile
      SubnetId: !Ref PublicSubnet1
      SecurityGroupIds:
        - !Ref WindmillInstallerSecurityGroup
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          set -e  # Exit on any error

          # Install required tools
          yum update -y
          yum install -y aws-cli jq postgresql15 aws-cfn-bootstrap

          # Set up logging directory with correct permissions
          mkdir -p /var/log/windmill-installer
          touch /var/log/windmill-installer/install.log

          # Create installation directory
          mkdir -p /opt/windmill-installer

          # Install kubectl
          curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
          chmod +x kubectl
          mv kubectl /usr/local/bin/

          # Install helm
          curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
          chmod +x get_helm.sh
          ./get_helm.sh

          # Create the installation script
          cat << 'EOF' > /opt/windmill-installer/install.sh
          #!/bin/bash
          set -e

          # Configure kubectl
          aws sts get-caller-identity > /dev/null
          export AWS_SDK_LOAD_CONFIG=1
          export KUBECONFIG=/root/.kube/config
          aws eks update-kubeconfig --name ${AWS::StackName}-cluster --region ${AWS::Region}

          # Add debugging for each kubectl attempt
          echo "HOME: $HOME"
          echo "KUBECONFIG: $KUBECONFIG"
          echo "User: $(whoami)"
          echo "AWS Identity: $(aws sts get-caller-identity)"
          echo "Trying kubectl command..."
          kubectl get nodes || echo "Command failed with status $?"

          echo "Waiting for EKS nodes to be ready..."
          while true; do
            # Force credential refresh on each attempt
            aws sts get-caller-identity > /dev/null
            aws eks update-kubeconfig --name ${AWS::StackName}-cluster --region ${AWS::Region}

            if kubectl get nodes &>/dev/null; then
              READY_NODES=$(kubectl get nodes -o json | jq -r '.items[] | select(.status.conditions[] | select(.type=="Ready" and .status=="True")) | .metadata.name' | wc -l)
              DESIRED_NODES=${NodeGroupSize}
              if [ "$READY_NODES" -eq "$DESIRED_NODES" ]; then
                echo "All nodes are ready"
                break
              fi
              echo "Found $READY_NODES ready nodes out of $DESIRED_NODES desired nodes"
            else
              echo "Waiting for cluster access..."
            fi
            sleep 30
          done

          echo "Installing AWS Load Balancer Controller..."
          # Install AWS Load Balancer Controller
          helm repo add eks https://aws.github.io/eks-charts
          helm repo update

          helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
            -n kube-system \
            --set clusterName=${AWS::StackName}-cluster \
            --set region=${AWS::Region} \
            --set vpcId=${VPC}

          echo "Waiting for AWS Load Balancer Controller to be ready..."
          kubectl wait --namespace kube-system \
            --for=condition=ready pod \
            --selector=app.kubernetes.io/name=aws-load-balancer-controller \
            --timeout=300s

          echo "Waiting for RDS to be available..."
          while true; do
            if pg_isready -h ${WindmillDB.Endpoint.Address} -p 5432 -U postgres 2>/dev/null; then
              echo "Database is ready"
              break
            fi
            echo "Database not ready yet..."
            sleep 30
          done

          echo "Creating namespace and installing Windmill..."
          kubectl create namespace windmill

          # Add helm repo and install Windmill
          helm repo add windmill https://windmill-labs.github.io/windmill-helm-charts
          helm repo update

          helm install ${AWS::StackName} windmill/windmill \
            --namespace windmill \
            --set windmill.databaseUrl="postgres://postgres:${DBPassword}@${WindmillDB.Endpoint.Address}/windmill?sslmode=require" \
            --set windmill.baseDomain=windmill.local \
            --set windmill.baseProtocol=http \
            --set windmill.appReplicas=${WorkerReplicas} \
            --set windmill.lspReplicas=2 \
            --set windmill.workerGroups[0].name=default \
            --set windmill.workerGroups[0].mode=worker \
            --set windmill.workerGroups[0].replicas=${WorkerReplicas} \
            --set windmill.workerGroups[1].name=native \
            --set windmill.workerGroups[1].mode=worker \
            --set windmill.workerGroups[1].replicas=${NativeWorkerReplicas} \
            --set windmill.app.service.spec.type=LoadBalancer \
            --set windmill.app.service.spec.sessionAffinity=None \
            --set windmill.app.service.port=8000 \
            --set windmill.app.service.ports[0].port=8000 \
            --set windmill.app.service.ports[0].targetPort=8000 \
            --set windmill.app.service.ports[0].protocol=TCP \
            --set postgresql.enabled=false \
            --set enterprise.enabled=${Enterprise}

          # Change service to LoadBalancer
          echo "Changing service to LoadBalancer"
          kubectl patch service windmill-app -n windmill -p '{"spec":{"type":"LoadBalancer","sessionAffinity":"None"}}'
          kubectl patch service windmill-app -n windmill -p '{"spec":{"ports":[{"name":"api","port":8000,"targetPort":8000,"protocol":"TCP"},{"name":"http","port":80,"targetPort":8000,"protocol":"TCP"}]}}'

          # Wait for LoadBalancer to get an address
          echo "Waiting for LoadBalancer address..."
          while true; do
            LB_HOSTNAME=$(kubectl get svc -n windmill windmill-app -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' 2>/dev/null)
            if [ ! -z "$LB_HOSTNAME" ]; then
              break
            fi
            echo "Waiting for LoadBalancer hostname..."
            sleep 30
          done

          # Store the LoadBalancer hostname in SSM
          aws ssm put-parameter \
            --region ${AWS::Region} \
            --name "/${AWS::StackName}/loadbalancer-hostname" \
            --value "$LB_HOSTNAME" \
            --type "String" \
            --overwrite

          # Signal CloudFormation that installation is complete
          echo "Signal CloudFormation that installation is complete"
          /opt/aws/bin/cfn-signal -e $? \
            --stack ${AWS::StackName} \
            --resource WindmillInstaller \
            --region ${AWS::Region}

          # Self-terminate this instance
          echo "Self-terminating instance"
          aws ec2 terminate-instances --instance-ids $(curl -s http://169.254.169.254/latest/meta-data/instance-id) --region ${AWS::Region}
          EOF

          # Set permissions and run the installation script
          chmod +x /opt/windmill-installer/install.sh

          # Run the installation script directly (not as ec2-user)
          cd /opt/windmill-installer && ./install.sh > /var/log/windmill-installer/install.log 2>&1

  LoadBalancerHostnameLookup:
    Type: Custom::SSMParameterLookup
    DependsOn: WindmillInstaller
    Properties:
      ServiceToken: !GetAtt LookupSSMParameterFunction.Arn
      ParameterName: !Sub "/${AWS::StackName}/loadbalancer-hostname"

  LookupSSMParameterFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Timeout: 300
      Runtime: nodejs18.x
      Code:
        ZipFile: !Sub |
          const { SSMClient, GetParameterCommand } = require('@aws-sdk/client-ssm');
          const response = require('cfn-response');

          exports.handler = async (event, context) => {
            if (event.RequestType === 'Delete') {
              return response.send(event, context, response.SUCCESS);
            }

            try {
              const ssmClient = new SSMClient();

              // Loop to check for the parameter until it's not "pending"
              let tries = 0;
              let paramValue = "pending";

              while (paramValue === "pending" && tries < 20) {
                const params = {
                  Name: event.ResourceProperties.ParameterName,
                  WithDecryption: false
                };

                const result = await ssmClient.send(new GetParameterCommand(params));
                paramValue = result.Parameter.Value;

                if (paramValue === "pending") {
                  await new Promise(resolve => setTimeout(resolve, 15000)); // wait 15 seconds
                  tries++;
                }
              }

              return response.send(event, context, response.SUCCESS, {
                HostnameValue: paramValue
              });
            } catch (error) {
              console.error(error);
              return response.send(event, context, response.FAILED, { error: error.message });
            }
          };

  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: SSMParameterAccess
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ssm:GetParameter
                Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AWS::StackName}/*"

  VPCCleanupFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt VPCCleanupRole.Arn
      Timeout: 300
      Runtime: nodejs18.x
      Code:
        ZipFile: |
          const { ElasticLoadBalancingClient, DescribeLoadBalancersCommand,
                  DeleteLoadBalancerCommand } = require('@aws-sdk/client-elastic-load-balancing');
          const response = require('cfn-response');

          exports.handler = async (event, context) => {
            if (event.RequestType !== 'Delete') {
              return response.send(event, context, response.SUCCESS);
            }

            try {
              const elb = new ElasticLoadBalancingClient();
              const vpcId = event.ResourceProperties.VpcId;

              // Find and delete Classic Load Balancers in the VPC
              const lbResponse = await elb.send(new DescribeLoadBalancersCommand({}));
              let deleted = false;

              for (const lb of lbResponse.LoadBalancerDescriptions || []) {
                if (lb.VPCId === vpcId) {
                  console.log(`Deleting Classic Load Balancer: ${lb.LoadBalancerName}`);
                  await elb.send(new DeleteLoadBalancerCommand({
                    LoadBalancerName: lb.LoadBalancerName
                  }));
                  deleted = true;
                }
              }

              if (deleted) {
                // Wait for deletion to complete
                console.log('Waiting 30 seconds for load balancer deletion to complete...');
                await new Promise(r => setTimeout(r, 30000));
              }

              return response.send(event, context, response.SUCCESS);
            } catch (error) {
              console.error('Error deleting load balancers:', error);
              return response.send(event, context, response.FAILED, {error: error.message});
            }
          };

  VPCCleanupRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: VPCCleanupPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ec2:DescribeAddresses
                  - ec2:DisassociateAddress
                  - ec2:DescribeNetworkInterfaces
                  - elasticloadbalancing:DescribeLoadBalancers
                  - elasticloadbalancing:DeleteLoadBalancer
                  - elasticloadbalancingv2:DescribeLoadBalancers
                  - elasticloadbalancingv2:DeleteLoadBalancer
                Resource: "*"

  VPCCleanup:
    Type: Custom::VPCCleanup
    Properties:
      ServiceToken: !GetAtt VPCCleanupFunction.Arn
      VpcId: !Ref VPC

Outputs:
  ClusterName:
    Description: EKS cluster name
    Value: !Sub "${AWS::StackName}-cluster"

  DatabaseEndpoint:
    Description: RDS instance endpoint
    Value: !GetAtt WindmillDB.Endpoint.Address

  LoadBalancerHostname:
    Description: Windmill LoadBalancer hostname
    Value: !Sub "http://${LoadBalancerHostnameLookup.HostnameValue}"
