#! /usr/bin/env nix-shell
#! nix-shell -i bash -p coreutils file gnugrep jq oci-cli openssh terraform
#
# usage: snatch-instance [PARAM...]
#
# Obtain a compute instance at OCI, and terminate once successful.
# This program might be useful when Oracle is out of host capacity.
#
# For possible values of PARAM search for "Available parameters" in this file.

set -efu

main() {(

  # Available parameters and their default values.
  # Change parameters via command line using following syntax:
  #   --NAME=VALUE
  # where NAME is one of param_NAME below.
  #
  # Example: --display_name=mycomputer
  param_apply=true
  param_availability_domain_list_json=
  param_display_name=computer
  param_shape=VM.Standard.A1.Flex
  param_memory_in_gbs=24
  param_ocpus=4
  param_ssh_authorized_key_file=$HOME/.ssh/id_rsa.pub
  param_workdir=
  param_compartment_id=
  param_source_id=
  param_subnet_id=

  # Set parameters from command line.
  for arg; do
    case $arg in
      --*=*)
        readonly "param_${arg#--}"
        ;;
      *)
        echo "$0: warning: ignoring unknown argument: $arg" >&2
    esac
  done

  #
  # Compute parameter defaults for each unset parameter.
  #

  if test -n "$param_apply"; then
    conf_apply=$param_apply
  else
    echo "$0: error: undefined parameter: $param_apply" >&2
    exit 2
  fi
  echo "$0: apply: $conf_apply" >&2

  if test -n "$param_display_name"; then
    conf_display_name=$param_display_name
  else
    echo "$0: error: undefined parameter: $param_display_name" >&2
    exit 2
  fi
  echo "$0: display_name: $conf_display_name" >&2

  if test -n "$param_ssh_authorized_key_file"; then
    if ! is_ssh_public_key "$param_ssh_authorized_key_file"; then
      echo "$0: error: not an ssh public key: $param_ssh_authorized_key_file" >&2
      exit 1
    fi
    conf_ssh_authorized_key=$(cat "$param_ssh_authorized_key_file")
  else
    echo "$0: error: undefined parameter: $param_ssh_authorized_key_file" >&2
    exit 2
  fi

  if test -n "$param_workdir"; then
    conf_workdir=$param_workdir
    mkdir -v -p "$conf_workdir"
  else
    conf_workdir=$(mktemp -d -t oci.snatch-instance.$$.XXXXXXXX)
    trap cleanup EXIT
    cleanup() {
      rm -v -R "$conf_workdir"
    }
  fi
  echo "$0: workdir: $conf_workdir" >&2

  if test -n "$param_compartment_id"; then
    conf_compartment_id=$param_compartment_id
  else
    conf_compartment_id=$(
      export response="$(
        oci iam compartment list \
            --lifecycle-state=ACTIVE \
      )"
      jq -enr '
        def assert(cond; msg): if cond then . else error(msg) end;

        env.response | fromjson |
        .data |
        assert(length == 1; "could not find exactly one compartment") |
        .[0] |
        .["compartment-id"]
      '
    )
  fi
  echo "$0: compartment_id: $conf_compartment_id" >&2

  if test -n "$param_availability_domain_list_json"; then
    conf_availability_domain_list_json=$param_availability_domain_list_json
  else
    conf_availability_domain_list_json=$(
      export response="$(
        oci iam availability-domain list \
            --compartment-id="$conf_compartment_id"
      )"
      jq -enr '
        env.response | fromjson |
        .data|map(.name)
      '
    )
    export conf_availability_domain_list_json
  fi
  echo "$0: availability_domain_list: $(jq -enr '
    env.conf_availability_domain_list_json | fromjson |
    join(", ")
  ')" >&2

  if test -n "$param_shape"; then
    conf_shape=$param_shape
  else
    echo "$0: error: undefined parameter: $param_shape" >&2
    exit 2
  fi
  echo "$0: shape: $conf_shape" >&2

  if test -n "$param_memory_in_gbs"; then
    conf_memory_in_gbs=$param_memory_in_gbs
  else
    echo "$0: error: undefined parameter: $param_memory_in_gbs" >&2
    exit 2
  fi
  echo "$0: memory_in_gbs: $conf_memory_in_gbs" >&2

  if test -n "$param_ocpus"; then
    conf_ocpus=$param_ocpus
  else
    echo "$0: error: undefined parameter: $param_ocpus" >&2
    exit 2
  fi
  echo "$0: ocpus: $conf_ocpus" >&2

  if test -n "$param_source_id"; then
    conf_source_id=$param_source_id
  else
    conf_source_id=$(
      export response="$(
        oci compute image list \
            --all \
            --lifecycle-state=AVAILABLE \
            --operating-system='Canonical Ubuntu' \
            --compartment-id="$conf_compartment_id" \
            --shape="$conf_shape" \
      )"

      jq -enr '
        env.response | fromjson |
        .data |
        sort_by(.["display-name"]) |
        last |
        .id
      '
    )
  fi
  echo "$0: source_id: $conf_source_id" >&2

  if test -n "$param_subnet_id"; then
    conf_subnet_id=$param_subnet_id
  else
    conf_subnet_id=$(
      export response="$(
        oci network subnet list \
            --compartment-id="$conf_compartment_id" \
            --lifecycle-state=AVAILABLE \
      )"
      jq -enr '
        def assert(cond; msg): if cond then . else error(msg) end;

        env.response | fromjson |
        .data |
        assert(length == 1; "could not find exactly one compartment") |
        .[0] |
        .id
      '
    )
  fi
  echo "$0: subnet_id: $conf_subnet_id" >&2

  # This tf_config will create a minimal (not applicable) configuration
  # which is sufficient to initalize terraform plugins.
  tf_config > "$conf_workdir"/main.tf.json

  terraform -chdir="$conf_workdir" init

  attempt=0
  until {
    attempt=$(expr $attempt + 1)
    availability_domain=$(
      jq -enr --argjson attempt "$attempt" '
        env.conf_availability_domain_list_json | fromjson |

        .[($attempt - 1) % length]
      '
    )
    tf_config \
        --availability_domain="$availability_domain" \
        --compartment_id="$conf_compartment_id" \
        --display_name="$conf_display_name" \
        --memory_in_gbs="$conf_memory_in_gbs" \
        --ocpus="$conf_ocpus" \
        --shape="$conf_shape" \
        --source_id="$conf_source_id" \
        --ssh_authorized_key="$conf_ssh_authorized_key" \
        --subnet_id="$conf_subnet_id" \
      > "$conf_workdir"/main.tf.json
    terraform -chdir="$conf_workdir" plan -out "$conf_workdir"/main.tfplan
    if test "$conf_apply" = true; then
      terraform -chdir="$conf_workdir" apply "$conf_workdir"/main.tfplan
    else
      echo "$0: Not not applying terraform plan" >&2
    fi
  }; do
    echo sleeping... >&2
    sleep 1m
  done
  echo "$0: $(date -Is): success on $attempt. attempt" >&2
)}

is_ssh_public_key() {
  {
    file -b "$1" | grep -Fq 'public key' &&
    ssh-keygen -l -f "$1"
  } >/dev/null 2>&1
}

# usage: tf_config [PARAM...]
# For possible values of PARAM see for "tf_config parameters" below.
tf_config() {(

  # tf_config parameters.
  # Change parameters via arguments using following syntax:
  #   --NAME=VALUE
  # where NAME is one of arg_NAME below.
  #
  # Example: --display_name=mycomputer
  arg_availability_domain=
  arg_compartment_id=
  arg_display_name=
  arg_memory_in_gbs=
  arg_ocpus=
  arg_shape=
  arg_source_id=
  arg_ssh_authorized_key=
  arg_subnet_id=

  for arg; do
    case $arg in
      --*=*)
        readonly "arg_${arg#--}"
        ;;
      *)
        echo "$0: warning: ignoring unknown argument: $arg" >&2
    esac
  done

  nix-instantiate \
      --eval --json --strict \
      --argstr availability_domain "$arg_availability_domain" \
      --argstr compartment_id "$arg_compartment_id" \
      --argstr display_name "$arg_display_name" \
      --argstr memory_in_gbs "$arg_memory_in_gbs" \
      --argstr ocpus "$arg_ocpus" \
      --argstr source_id "$arg_source_id" \
      --argstr shape "$arg_shape" \
      --argstr ssh_authorized_key "$arg_ssh_authorized_key" \
      --argstr subnet_id "$arg_subnet_id" \
      -E \
  '
    { availability_domain
    , compartment_id
    , display_name
    , memory_in_gbs
    , ocpus
    , shape
    , source_id
    , ssh_authorized_key
    , subnet_id
    }:
    {
      terraform.required_providers.oci = {
        source = "oracle/oci";
      };
      provider.oci = {};
      resource.oci_core_instance.generated_oci_core_instance = {
        agent_config = {
          is_management_disabled = "true";
          is_monitoring_disabled = "true";
          plugins_config = [
            {
              desired_state = "DISABLED";
              name = "Vulnerability Scanning";
            }
            {
              desired_state = "DISABLED";
              name = "Compute Instance Monitoring";
            }
            {
              desired_state = "DISABLED";
              name = "Bastion";
            }
          ];
        };
        availability_config = {
          is_live_migration_preferred = "true";
          recovery_action = "RESTORE_INSTANCE";
        };
        availability_domain = availability_domain;
        compartment_id = compartment_id;
        create_vnic_details = {
          assign_private_dns_record = "true";
          assign_public_ip = "true";
          subnet_id = subnet_id;
        };
        display_name = display_name;
        instance_options = {
          are_legacy_imds_endpoints_disabled = "false";
        };
        metadata = {
          ssh_authorized_keys = ssh_authorized_key;
        };
        shape = shape;
        shape_config = {
          memory_in_gbs = memory_in_gbs;
          ocpus = ocpus;
        };
        source_details = {
          source_id = source_id;
          source_type = "image";
        };
      };
    }
  '
)}

main "$@"