# V1
#
# Launches the Tailscale gokrazy appliance AMI. Unlike single_instance_stack.yml
# (Ubuntu + cloud-init running "tailscale up"), the appliance has no shell: its
# tailscaled reads the EC2 user-data directly via --config=vm:user-data. So the
# UserData below IS the tailscaled HuJSON config file (ipn.ConfigVAlpha,
# "version":"alpha0"), assembled from the parameters.

Parameters:
  AuthKey:
    Description: Machine auth key for joining machine to tailnet.
    Type: String
    NoEcho: true
    AllowedPattern: tskey-auth-[a-zA-Z0-9\-]+
    ConstraintDescription: valid machine auth key starting with tskey-auth-
  Hostname:
    Description: The name to use for this host on your tailnet.
    Type: String
    MinLength: '2'
    MaxLength: '63'
    AllowedPattern: '[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?'
    ConstraintDescription: letters, numbers and hyphens only; must start and end with a letter or number.
  RemoteConfig:
    Description: >-
      Delegate full remote control of this node to the tailnet admin (the
      control plane can read and change any pref, no local consent). Only for
      admin-owned fleet devices; not for personal/BYOD. The control server
      cannot drive this yet, but it can be set here.
    Type: String
    Default: 'false'
    AllowedValues: ['true', 'false']
  EnableSSH:
    Description: Run the Tailscale SSH server on this node.
    Type: String
    Default: 'true'
    AllowedValues: ['true', 'false']
  AdvertiseExitNode:
    Description: Advertise this node as an exit node (advertises 0.0.0.0/0 and ::/0).
    Type: String
    Default: 'false'
    AllowedValues: ['true', 'false']
  AdvertiseRoutes:
    Description: >-
      Subnet routes to advertise, as a quoted comma-separated list of CIDRs,
      e.g. "10.0.0.0/8","192.168.1.0/24". Leave blank for none.
    Type: String
    Default: ''
    AllowedPattern: '^$|^"[^"]+"(,"[^"]+")*$'
    ConstraintDescription: quoted, comma-separated CIDRs (e.g. "10.0.0.0/8","192.168.1.0/24") or blank.
  AcceptRoutes:
    Description: Accept subnet routes advertised by other nodes.
    Type: String
    Default: 'false'
    AllowedValues: ['true', 'false']
  AutoUpdate:
    Description: Automatically check for and apply client updates.
    Type: String
    Default: 'true'
    AllowedValues: ['true', 'false']
  InstanceType:
    Description: EC2 instance type. Smallest sizes are intentionally excluded.
    Type: String
    Default: t3.medium
    AllowedValues:
      - t3.medium
      - t3.large
      - t3.xlarge
      - t3.2xlarge
      - m5.large
      - m5.xlarge
      - m6i.large
      - m6i.xlarge
      - c5.large
      - c6i.large
    ConstraintDescription: must be one of the supported EC2 instance types.
  VpcId:
    Description: ID of an existing Virtual Private Cloud (leave blank to use default VPC).
    Type: String
  VpcSubnetId:
    Description: ID of an existing public subnet within the specified VPC (leave blank to use default VPC).
    Type: String

# RegionMap maps each supported region to the public gokrazy appliance AMI in
# that region; ImageId resolves via !FindInMap on the stack's own region, so the
# launch works wherever the user opens it (and fails closed in unlisted regions).
# The release build regenerates these ids when it publishes new AMIs; the
# placeholders below are filled in by that pipeline.
Mappings:
  RegionMap:
    us-east-1: { AMI: ami-000000000000appl } # TODO: publish + fill per release
    us-east-2: { AMI: ami-000000000000appl }
    us-west-2: { AMI: ami-000000000000appl }
    eu-west-1: { AMI: ami-000000000000appl }
    eu-central-1: { AMI: ami-0ecbb051897ffa1cb }
    ap-southeast-1: { AMI: ami-000000000000appl }
    ap-southeast-2: { AMI: ami-000000000000appl }
    ap-northeast-1: { AMI: ami-000000000000appl }

Conditions:
  UseDefaultSubnet: !Or [!Equals [!Ref VpcId, ""], !Equals [!Ref VpcSubnetId, ""]]
  UseSpecificSubnet: !And [!Not [!Equals [!Ref VpcId, ""]], !Not [!Equals [!Ref VpcSubnetId, ""]]]
  ExitNodeOn: !Equals [!Ref AdvertiseExitNode, "true"]
  HasRoutes: !Not [!Equals [!Ref AdvertiseRoutes, ""]]

Resources:
  SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Disable all inbound traffic
      VpcId: !Ref VpcId
      SecurityGroupIngress: []

  DefaultInstance:
    Type: AWS::EC2::Instance
    Condition: UseDefaultSubnet
    Properties:
      ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", AMI]
      InstanceType: !Ref InstanceType
      SecurityGroupIds:
        - !GetAtt [SecurityGroup, GroupId]
      # UserData is the tailscaled HuJSON config (ipn.ConfigVAlpha); the
      # appliance reads it via --config=vm:user-data. RoutesJSON is the inner
      # AdvertiseRoutes array body: exit-node defaults and/or the subnet CIDRs,
      # each omitted (AWS::NoValue) when off, joined with a comma.
      UserData: &ApplianceUserData
        Fn::Base64:
          Fn::Sub:
            - |
              {
                "version": "alpha0",
                "authKey": "${AuthKey}",
                "hostname": "${Hostname}",
                "acceptRoutes": ${AcceptRoutes},
                "RunSSHServer": ${EnableSSH},
                "RemoteConfig": ${RemoteConfig},
                "AutoUpdate": { "Check": ${AutoUpdate}, "Apply": ${AutoUpdate} },
                "AdvertiseRoutes": [${RoutesJSON}]
              }
            - RoutesJSON:
                Fn::Join:
                  - ","
                  - - !If [ExitNodeOn, '"0.0.0.0/0","::/0"', !Ref "AWS::NoValue"]
                    - !If [HasRoutes, !Ref AdvertiseRoutes, !Ref "AWS::NoValue"]

  SpecificInstance:
    Type: AWS::EC2::Instance
    Condition: UseSpecificSubnet
    Properties:
      ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", AMI]
      InstanceType: !Ref InstanceType
      SecurityGroupIds:
        - !GetAtt [SecurityGroup, GroupId]
      SubnetId: !Ref VpcSubnetId
      UserData: *ApplianceUserData
