package core.aws.task.ec2;

import com.amazonaws.services.ec2.model.CreateImageRequest;
import com.amazonaws.services.ec2.model.CreateImageResult;
import com.amazonaws.services.ec2.model.CreateSecurityGroupRequest;
import com.amazonaws.services.ec2.model.CreateTagsRequest;
import com.amazonaws.services.ec2.model.DescribeImagesRequest;
import com.amazonaws.services.ec2.model.DescribeImagesResult;
import com.amazonaws.services.ec2.model.InstanceType;
import com.amazonaws.services.ec2.model.IpPermission;
import com.amazonaws.services.ec2.model.RunInstancesRequest;
import com.amazonaws.services.ec2.model.Subnet;
import core.aws.client.AWS;
import core.aws.env.Context;
import core.aws.env.Environment;
import core.aws.resource.ec2.Instance;
import core.aws.resource.ec2.InstanceState;
import core.aws.resource.ec2.KeyPair;
import core.aws.resource.image.Image;
import core.aws.task.linux.AnsibleProvisioner;
import core.aws.util.Asserts;
import core.aws.util.Lists;
import core.aws.util.Threads;
import core.aws.workflow.Action;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * @author neo
 */
@Action("bake")
public class BakeAMITask extends core.aws.workflow.Task<Image> {
    private final Logger logger = LoggerFactory.getLogger(BakeAMITask.class);

    private final Instance resumeBakeInstance;

    private String resourceId;
    private KeyPairHelper keyPairHelper;
    private EC2TagHelper tagHelper;
    private Subnet bakeSubnet;

    public BakeAMITask(Image image, Instance resumeBakeInstance) {
        super(image);
        this.resumeBakeInstance = resumeBakeInstance;
    }

    @Override
    public void execute(Context context) throws Exception {
        tagHelper = new EC2TagHelper(context.env);
        keyPairHelper = new KeyPairHelper(context.env);

        String bakeSubnetId = context.env.bakeSubnetId;
        if (bakeSubnetId != null) {
            bakeSubnet = AWS.vpc.describeSubnets(Lists.newArrayList(bakeSubnetId)).get(0);
            if (!bakeSubnet.isMapPublicIpOnLaunch())
                throw new Error("bake subnet does not auto assign public ip, please check the subnet setting, subnetId=" + bakeSubnetId);
        }

        resourceId = "ami-" + resource.id + LocalDateTime.now().format(DateTimeFormatter.ofPattern("-yyyyMMdd-HHmm"));

        com.amazonaws.services.ec2.model.Instance instance;

        if (resumeBakeInstance != null) {
            instance = resumeBakeInstance.remoteInstances.get(0);
            logger.info("resume bake with {}, instanceId={}", resumeBakeInstance.id, instance.getInstanceId());
            Asserts.isTrue(InstanceState.RUNNING.equalsTo(instance.getState()), "resume bake instance must be running, instanceId={}, currentState={}", instance.getInstanceId(), instance.getState().getName());
        } else {
            KeyPair keyPair = createKeyPair(context.env);
            String sgId = createSG(context.env);
            instance = createInstance(keyPair, sgId);
        }

        AnsibleProvisioner provisioner = new AnsibleProvisioner(context.env, instance, resource.playbook, resource.packageDir);
        provisioner.additionalVariables.put("tomcat_service_state", "stopped");
        provisioner.additionalVariables.put("supervisor_service_state", "stopped");
        provisioner.additionalVariables.put("nginx_service_state", "stopped");
        provisioner.additionalVariables.put("elasticsearch_heap_size", "1g");
        provisioner.provision();

        String imageId = createAMI(context, instance.getInstanceId());

        AWS.ec2.terminateInstances(Lists.newArrayList(instance.getInstanceId()));

        keyPairHelper.deleteKeyPair(instance.getKeyName());
        AWS.ec2.deleteSecurityGroup(instance.getSecurityGroups().get(0).getGroupId());
        waitUntilAMIFinished(imageId);
    }

    private void waitUntilAMIFinished(String imageId) throws InterruptedException {
        logger.info("wait until AMI finished, imageId={}", imageId);
        while (true) {
            DescribeImagesResult result = AWS.ec2.ec2.describeImages(new DescribeImagesRequest().withImageIds(imageId));
            String state = result.getImages().get(0).getState();
            logger.info("AMI state {} => {}", imageId, state);
            if ("available".equals(state)) {
                break;
            } else if ("failed".equals(state)) {
                throw new IllegalStateException("AMI failed to create, please check AWS console for more details");
            }
            Threads.sleepRoughly(Duration.ofSeconds(30));
        }
    }

    private String createAMI(Context context, String instanceId) throws Exception {
        AWS.ec2.stopInstances(Lists.newArrayList(instanceId));

        logger.info("create AMI, instanceId={}, imageName={}", instanceId, resource.name());
        CreateImageResult result = AWS.ec2.ec2.createImage(new CreateImageRequest(instanceId, resource.name()));
        String imageId = result.getImageId();

        AWS.ec2.createTags(new CreateTagsRequest()
            .withResources(imageId)
            .withTags(tagHelper.env(),
                tagHelper.resourceId(resource.id),
                tagHelper.version(resource.nextVersion()),
                tagHelper.name(resource.id + ":" + resource.nextVersion())));

        String key = "ami/" + resource.id;
        context.output(key, String.format("imageId=%s", imageId));
        logger.info("result imageId => {}", imageId);
        return imageId;
    }

    private com.amazonaws.services.ec2.model.Instance createInstance(KeyPair keyPair, String sgId) throws Exception {
        RunInstancesRequest request = new RunInstancesRequest()
            .withKeyName(keyPair.remoteKeyPair.getKeyName())
            .withInstanceType(InstanceType.C4Large)
            .withImageId(resource.baseAMI.imageId())
            .withMinCount(1)
            .withMaxCount(1)
            .withSecurityGroupIds(sgId);

        if (bakeSubnet != null) request.withSubnetId(bakeSubnet.getSubnetId());

        return AWS.ec2.runInstances(request,
            tagHelper.name(resourceId),
            tagHelper.env(),
            tagHelper.resourceId(resourceId),
            tagHelper.type("ami"),
            tagHelper.amiImageId(resource.id())).get(0);
    }

    private KeyPair createKeyPair(Environment env) throws IOException {
        KeyPair keyPair = new KeyPair(resourceId, env.name + ":" + resourceId);
        if (!AWS.ec2.keyPairExists(keyPair.name)) {   // for resume previous failed baking, key pair may exist
            keyPairHelper.createKeyPair(keyPair);
        }
        return keyPair;
    }

    private String createSG(Environment env) throws Exception {
        String sgName = env.name + ":" + resourceId;
        CreateSecurityGroupRequest request = new CreateSecurityGroupRequest(sgName, sgName);
        if (bakeSubnet != null) request.setVpcId(bakeSubnet.getVpcId());

        String sgId = AWS.ec2.createSecurityGroup(request).getGroupId();
        AWS.ec2.createSGIngressRules(sgId, Lists.newArrayList(new IpPermission()
            .withIpRanges("0.0.0.0/0")
            .withFromPort(22)
            .withToPort(22)
            .withIpProtocol("tcp")));

        AWS.ec2.createTags(new CreateTagsRequest()
            .withResources(sgId)
            .withTags(tagHelper.name(resourceId), tagHelper.env(), tagHelper.resourceId(resourceId)));

        return sgId;
    }
}

@@ -5878,17 +5878,17 @@
 nceType.
-C
+M
 4Large)%0A

use m4.large to bake for balanced cpu/ram