/**
 * Gitlab Runner Autoscaler using docker-autoscaler
 * -------------------------------------
 * This implementation uses a single EC2 instance that executes a gitlab runner leveraging the
 * 'docker-autoscaler' executor and acting as runner manager.
 * Both the manager and runners must have the Docker Engine installed.
 * The manager connects to the runners through the instance's default user, but can be set otherwise.
 * Runners must be set up so that the user the manager connects with can access the Docker Engine socket (i.e. adding)
 * it to the 'docker' group).
 * Runners are created and deleted by means of an AutoScaling Group that the manager controls.
 * Container images are pulled by the manager and sent to the runners through Docker pipes.
 *
 * Requirements:
 *   - An EC2 instance with Docker Engine to act as manager.
 *   - A Launch Template:
 *     - referencing an AMI equipped with Docker Engine for the runners to use, or
 *     - using any AMI but providing userData so that the Docker Engine is installed and configured "properly".
 *   - An AutoScaling Group with Minimum and Desired capacity set to 0.
 *   - Permissions to discover and scale the ASG (manager).
 *   - ECR authentication (manager).
 *   - ECR read only access to pull images from used repositories (manager).
 *
 * Pulumi resources info:
 *   - https://www.pulumi.com/registry/packages/aws/api-docs/ec2/securitygroup/
 *   - https://www.pulumi.com/registry/packages/aws/api-docs/vpc/securitygroupingressrule/
 *   - https://www.pulumi.com/registry/packages/aws/api-docs/vpc/securitygroupegressrule/
 *   - https://www.pulumi.com/registry/packages/aws/api-docs/autoscaling/group/
 *   - https://www.pulumi.com/registry/packages/aws/api-docs/ec2/launchtemplate/
 *   - https://www.pulumi.com/registry/packages/aws/api-docs/ec2/instance/
 **/

import * as aws from "@pulumi/aws";
import * as cloudinit from "@pulumi/cloudinit";
import * as pulumi from "@pulumi/pulumi";
import * as yaml from "yaml";

const awsRegion_output = aws.getRegionOutput();
const callerIdentity_output = aws.getCallerIdentity();

const config = new pulumi.Config();
const gitlab_url = config.require("gitlab-url");
const token = config.requireSecret("gitlab-runner-token");

const ami_amazonLinux_arm64_latest = aws.ec2.getAmiOutput({
    owners: [ "amazon" ],
    nameRegex: "^al2023-ami-2023.*",
    filters: [
        {
            name: "architecture",
            values: [ "arm64" ],
        },
        {
            name: "state",
            values: [ "available" ],
        },
    ],
    mostRecent: true,
});
const ami_amazonLinux_x86_64_latest = aws.ec2.getAmiOutput({
    owners: [ "amazon" ],
    nameRegex: "^al2023-ami-2023.*",
    filters: [
        {
            name: "architecture",
            values: [ "x86_64" ],
        },
        {
            name: "state",
            values: [ "available" ],
        },
    ],
    mostRecent: true,
});

const subnet_ids = aws.ec2.getSubnetsOutput({
    filters: [{
        name: "tag:Name",
        values: [
            // "private_a",
            // "private_b",
            // "private_c",
            "private-eu-west-1a",
            "private-eu-west-1b",
            "private-eu-west-1c",
        ],
    }],
}).apply(subnets => subnets.ids);

// manager's security group - start
const gitlab_runner_autoscalingManager_securityGroup = new aws.ec2.SecurityGroup(
    "gitlab-runner-autoscalingManager",
    {
        name: "Gitlab Runner autoscaling manager",
        description: "Security perimeter for the Gitlab Runner autoscaling manager",
        tags: {
            Name: "Gitlab Runner autoscaling manager",
        },
    },
);
new aws.vpc.SecurityGroupIngressRule(
    "gitlab-runner-autoscalingManager-fullAccess",
    {
        securityGroupId: gitlab_runner_autoscalingManager_securityGroup.id,
        description: "Allow all",
        cidrIpv4: "0.0.0.0/0",
        ipProtocol: "-1",
    },
);
new aws.vpc.SecurityGroupEgressRule(
    "gitlab-runner-autoscalingManager-fullAccess",
    {
        securityGroupId: gitlab_runner_autoscalingManager_securityGroup.id,
        description: "Allow all",
        cidrIpv4: "0.0.0.0/0",
        ipProtocol: "-1",
    },
);
// manager's security group - end

// runners' security group - start
const gitlab_runners_securityGroup = new aws.ec2.SecurityGroup(
    "gitlab-runners",
    {
        name: "Gitlab Runners",
        description: "Security perimeter for the Gitlab Runners",
        tags: {
            Name: "Gitlab Runners",
        },
    },
);
new aws.vpc.SecurityGroupIngressRule(  // FIXME: reduce?
    "gitlab-runners-managerAccess",
    {
        securityGroupId: gitlab_runners_securityGroup.id,
        description: "Allow all from Gitlab Runner autoscaling manager",
        referencedSecurityGroupId: gitlab_runner_autoscalingManager_securityGroup.id,
        ipProtocol: "-1",
    },
);
new aws.vpc.SecurityGroupEgressRule(  // FIXME: reduce?
    "gitlab-runners-internetAccess",
    {
        securityGroupId: gitlab_runners_securityGroup.id,
        description: "Allow all",
        cidrIpv4: "0.0.0.0/0",
        ipProtocol: "-1",
    },
);
// runners' security group - end

// runners - start
const gitlab_runners_userData = new cloudinit.Config(
    "gitlab-runners",
    {
        base64Encode: true,  // required by the launch template
        parts: [{
            filename: "cloud-config.docker-engine.yml",
            contentType: "text/cloud-config",
            content: yaml.stringify({
                packages: [ "docker" ],
                runcmd: [
                    "systemctl daemon-reload",
                    "systemctl enable --now docker.service",
                    "grep docker /etc/group -q && usermod -a -G docker ec2-user"
                ],
            }),
        }],
    },
);
const gitlab_runners_launchTemplate = new aws.ec2.LaunchTemplate(
    "gitlab-runners",
    {
        name: "GitlabRunners",
        description: "Gitlab Runner instances managed using the docker-autoscaler executor",
        updateDefaultVersion: true,
        imageId: ami_amazonLinux_x86_64_latest.apply(amis => amis.id),
        vpcSecurityGroupIds: [ gitlab_runners_securityGroup.id ],
        userData: gitlab_runners_userData.rendered,
        tagSpecifications: [
            {
                resourceType: "instance",
                tags: {
                    Name: "Gitlab Runner",
                    Owner: "infra@example.org",
                    Team: "Infra",
                },
            },
            {
                resourceType: "volume",
                tags: {
                    Name: "Gitlab Runner",
                    Owner: "infra@example.org",
                    Team: "Infra",
                },
            },
        ],
    },
);
const gitlab_runners_autoScalingGroup = new aws.autoscaling.Group(
    "gitlab-runners",
    {
        name: "GitlabRunners",
        tags: [
            {
                key: "AutoScalingGroup",
                value: "GitlabRunners",
                propagateAtLaunch: true,
            },
            {
                key: "Executor",
                value: "docker-autoscaler",
                propagateAtLaunch: true,
            },
        ],
        vpcZoneIdentifiers: subnet_ids,
        minSize: 0,
        maxSize: 2,
        desiredCapacity: 0,
        mixedInstancesPolicy:{
            instancesDistribution: {
                onDemandBaseCapacity: 1,
                onDemandPercentageAboveBaseCapacity: 0,

                // be mindful of prices
                // https://docs.aws.amazon.com/autoscaling/ec2/userguide/allocation-strategies.html#spot-allocation-strategy
                spotAllocationStrategy: "price-capacity-optimized",
            },
            launchTemplate: {
                launchTemplateSpecification: {
                    launchTemplateId: gitlab_runners_launchTemplate.id,
                    version: "$Latest",
                },
                overrides: [
                    { instanceType: aws.ec2.InstanceType.M6a_XLarge },
                    { instanceType: aws.ec2.InstanceType.M6i_XLarge },
                    { instanceType: aws.ec2.InstanceType.M7a_XLarge },
                    { instanceType: aws.ec2.InstanceType.M7i_XLarge },
                ],
            },
        },
    },
    {
        ignoreChanges: [
            "desiredCapacity",  // managed by autoscaling manager
        ],
    },
);
// runners - end

// manager - start
const gitlab_runner_autoscalingManager_role = new aws.iam.Role(
    "gitlab-runner-autoscalingManager",
    {
        name: "GitlabRunnerAutoscalingManager",
        description: "Allow Gitlab Runner autoscaling managers to scale runner instances",
        assumeRolePolicy: JSON.stringify({
            Version: "2012-10-17",
            Statement: [{
                Sid: "AllowEc2ToAssumeThisVeryRole",
                Effect: "Allow",
                Principal: {
                    Service: "ec2.amazonaws.com",
                },
                Action: "sts:AssumeRole",
            }],
        }),
        managedPolicyArns: [
            "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore",  // instance management via SSM
        ],
    },
);
pulumi.all([
    gitlab_runners_autoScalingGroup.arn,
    gitlab_runners_autoScalingGroup.name,
    awsRegion_output,
    callerIdentity_output,
]).apply(
    ([ asgArn, asgName, awsRegion, callerIdentity ]) => new aws.iam.RolePolicy(
        "gitlab-runner-autoscalingManager-inline-allowRoleFunctions",
        {
            role: gitlab_runner_autoscalingManager_role,
            name: "AllowRoleFunctions",
            policy: JSON.stringify({
                Version: "2012-10-17",
                Statement: [
                    {
                        Sid: "AllowAsgDiscovering",
                        Effect: "Allow",
                        Action: [
                            "autoscaling:DescribeAutoScalingGroups",
                            "ec2:DescribeInstances",
                        ],
                        Resource: "*"
                    },
                    {
                        Sid: "AllowAsgScaling",
                        Effect: "Allow",
                        Action: [
                            "autoscaling:SetDesiredCapacity",
                            "autoscaling:TerminateInstanceInAutoScalingGroup"
                        ],
                        Resource: asgArn,
                    },
                    {
                        Sid: "AllowManagingAccessToAsgInstances",
                        Effect: "Allow",
                        Action: "ec2-instance-connect:SendSSHPublicKey",
                        Resource: `arn:aws:ec2:${awsRegion.name}:${callerIdentity.accountId}:instance/*`,
                        Condition: {
                            StringEquals: {
                                "ec2:ResourceTag/aws:autoscaling:groupName": asgName,
                            },
                        },
                    },
                    {
                        Sid: "AllowAuthenticatingToEcr",
                        Effect: "Allow",
                        Action: "ecr:GetAuthorizationToken",
                        Resource: "*",
                    },
                    {
                        Sid: "AllowPullingImagesFromEcr",
                        Effect: "Allow",
                        Action: [
                            "ecr:BatchGetImage",
                            "ecr:GetDownloadUrlForLayer",
                        ],
                        Resource: "*",
                    },
                ],
            }),
        },

    ),
);
const gitlab_runner_autoscalingManager_instanceProfile = new aws.iam.InstanceProfile(
    "gitlab-runner-autoscalingManager",
    {
        name: "GitlabRunnerAutoscalingManager",
        role: gitlab_runner_autoscalingManager_role,
    },
    { protect: true }
);
const gitlab_runner_autoscalingManager_userData = pulumi.all([
    gitlab_runners_autoScalingGroup.name,
    awsRegion_output,
    callerIdentity_output,
    gitlab_url,
    token,
]).apply(
    ([ asgName, awsRegion, callerIdentity, gitlabUrl, token ]) => new cloudinit.Config(
        "gitlab-runner-autoscalingManager",
        {
            parts: [
                {
                    filename: "cloud-config.docker-engine.yml",
                    contentType: "text/cloud-config",
                    content: yaml.stringify({
                        package_upgrade: false,
                        packages: [
                            "docker",
                            "amazon-ecr-credential-helper",
                        ],
                        write_files: [
                            {
                                path: "/root/.docker/config.json",
                                permissions: "0644",
                                content: `{ "credsStore": "ecr-login" }`,
                            },
                        ],
                        runcmd: [
                            "systemctl daemon-reload",
                            "systemctl enable --now docker.service",
                        ],
                    }),
                },
                {
                    filename: "cloud-config.gitlab-runner.yml",
                    mergeType: "dict(recurse_array,no_replace)+list(append)",
                    contentType: "text/cloud-config",
                    content: yaml.stringify({
                        package_upgrade: false,
                        yum_repos: {
                            "gitlab-runner": {
                                name: "Gitlab Runner",
                                baseurl: "https://packages.gitlab.com/runner/gitlab-runner/amazon/2023/$basearch",
                                gpgcheck: true,
                                gpgkey: [
                                    "https://packages.gitlab.com/runner/gitlab-runner/gpgkey",
                                    "https://packages.gitlab.com/runner/gitlab-runner/gpgkey/runner-gitlab-runner-4C80FB51394521E9.pub.gpg",
                                    "https://packages.gitlab.com/runner/gitlab-runner/gpgkey/runner-gitlab-runner-49F16C5CC3A0F81F.pub.gpg",
                                ].join("\n"),
                                sslverify: true,
                                sslcacert: "/etc/pki/tls/certs/ca-bundle.crt",
                                metadata_expire: 300,
                            },
                        },
                        write_files: [
                            {
                                path: "/etc/gitlab-runner/config.toml",
                                permissions: "0600",
                                content: [
                                    `concurrent = 1`,
                                    `check_interval = 0`,
                                    `shutdown_timeout = 0`,
                                    ``,
                                    `[session_server]`,
                                    `  session_timeout = 1800`,
                                    ``,
                                    `[[runners]]`,
                                    `  name = "runner autoscaler"`,
                                    ``,
                                    `  url = "${gitlabUrl}"`,
                                    `  token = "${token}"`,
                                    ``,
                                    `  executor = "docker-autoscaler"`,
                                    `  environment = [ "AWS_REGION=${awsRegion.name}" ]`,
                                    ``,
                                    `  [runners.docker]`,
                                    `    privileged = false`,
                                    ``,
                                    `    image = "${callerIdentity.accountId}.dkr.ecr.${awsRegion.name}.amazonaws.com/some-repo/busybox:latest"`,
                                    `    pull_policy = [`,
                                    `      "if-not-present",`,
                                    `      "always"`,
                                    `    ]`,
                                    `    allowed_pull_policies = [`,
                                    `      "if-not-present",`,
                                    `      "always",`,
                                    `      "never"`,
                                    `    ]`,
                                    ``,
                                    `  [runners.autoscaler]`,
                                    `    plugin = "aws"`,
                                    ``,
                                    `    [runners.autoscaler.plugin_config]`,
                                    `      name = "${asgName}"`,
                                    ``,
                                    `    [[runners.autoscaler.policy]]`,
                                    `      periods = [ "* 7-19 * * mon-fri" ]`,
                                    `      timezone = "Europe/Amsterdam"`,
                                    `      idle_count = 1`,
                                    `      idle_time = "20m0s"`,
                                ].join("\n"),  // FIXME: granted, this sucks but at least I can interpolate in it
                            },
                            {
                                path: "/root/.aws/config",
                                permissions: "0600",
                                content: [
                                    `[default]`,
                                    `region = ${awsRegion.name}`,
                                ].join("\n"),
                            },
                        ],
                        packages: [ "gitlab-runner-17.4.0" ],
                        runcmd: [
                            "systemctl daemon-reload",
                            "systemctl enable --now 'gitlab-runner'",
                            "gitlab-runner fleeting install",
                        ],
                    }),
                },
            ],
        },

    ),
);
new aws.ec2.Instance(
    "gitlab-runner-autoscalingManager",
    {
        tags: {
            Name: "Gitlab Runner autoscaling manager",
        },
        ami: ami_amazonLinux_arm64_latest.apply(ami => ami.id),
        instanceType: aws.ec2.InstanceType.T4g_Micro,
        iamInstanceProfile: gitlab_runner_autoscalingManager_instanceProfile,
        subnetId: subnet_ids[0],
        associatePublicIpAddress: false,
        vpcSecurityGroupIds: [ gitlab_runner_autoscalingManager_securityGroup.id ],
        userData: gitlab_runner_autoscalingManager_userData.rendered,
    },
);
// manager - end
