#!/usr/bin/env nu 
# Info: Prepare for etcd installation
# Author: JesusPerezLorenzo 
# Release: 1.0.2
# Date: 26-02-2024

use lib_provisioning/cmd/env.nu * 
use lib_provisioning/cmd/lib.nu *
use lib_provisioning/utils/ui.nu *
use lib_provisioning/utils/files.nu find_file
use lib_provisioning/sops *

def get_domain_name [
  defs: record
  source: string 
] {
  match $source {
    "$defaults" => $defs.server.main_domain,
    _ =>  $source
  }
}
def openssl_ecc_cert [
  defs: record
  src: string
  run_root: string
  cluster_name: string
  hostname: string
  signature: string
  long_sign: int
] { 
  let etcd_cn = ( $defs.taskserv.cn | default "")
  let ca_signature = ($defs.taskserv.ca_sign | default "")
  let ssl_curve = ($defs.taskserv.ssl_curve | default "")
  let sign_sha = ($defs.taskserv.sign_sha | default "")
  let sign_cipher = ($defs.taskserv.cipher | default "")
  let sign_days = ($defs.taskserv.sign_days | default "")

  let on_error =  { |msg: string| 
    print $"🛑 (_ansi red)Error(_ansi reset) (_ansi yellow)ECC(_ansi reset): ($msg)" 
    rm -f ($src | path join "pass")
  }
  ^openssl ecparam -genkey -name $ssl_curve -out ($src | path join $"($cluster_name).key") | ignore
  let res = (^openssl req -new $"-SHA($sign_sha)" -key ($src | path join $"($cluster_name).key") -nodes
  -out ($src | path join $"($cluster_name).csr")
  -subj $"/CN=($etcd_cn)" -config ($src | path join "openssl.conf") -extensions ssl_peer
  | complete )
  if $res.exit_code != 0 { 
    do $on_error $"openssl csr error ($res.stdout)"  
    exit 1
  } 
  let res = (^openssl x509 -req $"-SHA($sign_sha)" -in ($src | path join $"($cluster_name).csr")
    -CA ($src | path join "ca.crt") -CAkey ($src | path join "ca.key") 
    -CAcreateserial -out ($src | path join $"($cluster_name).crt") -days $sign_days
    -extensions ssl_peer -extfile ($src | path join "openssl.conf")
  | complete )
  if $res.exit_code != 0 { 
    do $on_error $"openssl x509 req error ($res.exit_code)($res.stdout)"  
    exit 1 
  } 
  ^openssl ecparam -genkey -name $ssl_curve -out ($src | path join $"($hostname).key")  | ignore
  let res = (^openssl req -noenc -new $"-SHA($sign_sha)" -key ($src | path join $"($hostname).key")
    -nodes -out ($src | path join $"($hostname).csr")   
    -subj $"/CN=($etcd_cn)" -config ($src | path join "openssl.conf") -extensions ssl_peer | complete )
  if res.exit_code != 0 and not ($src |  path join $"($hostname).csr"  | path exists) {
      do $on_error $"🛑 openssl req csr error ($res.exit_code) ($res.stdout)"  
      exit 1
  } 
  let res = (^openssl x509 -req -noenc $"-SHA($sign_sha)" -in ($src | path join $"($hostname).csr") 
    -CA ($src | path join "ca.crt") -CAkey ($src | path join "ca.key")
    -CAcreateserial -out ($src | path join $"($hostname).crt") -days $sign_days
    -extensions ssl_peer -extfile ($src | path join "openssl.conf")
  | complete )
  if res.exit_code != 0 and not ($src |  path join $"($hostname).crt"  | path exists) {
    do $on_error $"🛑 openssl x509 req  error ($res.stdout)"  
    exit 1
  }
}
def openssl_rsa_cert [
  defs: record
  src: string
  run_root: string
  cluster_name: string
  hostname: string
  signature: string
  long_sign: int
] { 
  let etcd_cn = ( $defs.taskserv.cn | default "")
  let sign_cipher = ($defs.taskserv.cipher | default "")
  let sign_days = ($defs.taskserv.sign_days | default "")

  let on_error =  { |msg: string| 
    print $"🛑 (_ansi red)Error(_ansi reset) (_ansi yellow)RSA(_ansi reset): ($msg)" 
    rm -f ($src | path join "pass")
  }
  if not ($src | path join "pass" | path exists) { $defs.taskserv.sign_pass | save -f ($src | path join "pass") }
  ^openssl genrsa -passout $"file:($src | path join "pass")" $sign_cipher -out ($src | path join $"($cluster_name)_p.key") $long_sign
  ^openssl rsa -in "$src/$cluster_name"_p.key -out ($src | path join $"($cluster_name).key")
  if not ($src | path join "openssl.conf" | path exists) { 
    do $on_error $"openssl.con not found in ($src |path join "openssl.conf")"
    exit 1
  }
  let res = (^openssl req -newkey rsa:($long_sign) -passout $"file:($src | path join "pass")" -key ($src | path join $"($cluster_name).key")
    -out ($src | path join $"($cluster_name).csr") 
    -subj $"/CN=($etcd_cn)" -config ($src | path join "openssl.conf") -extensions ssl_client
  | complete)
  if $res.exit_code != 0 {
    do $on_error $"openssl req error ($res.exit_code) ($res.stdout)"
    exit 1
  } 
  print $"openssl gemrsa error ($res.exit_code) ($res.stdout)"
  (^openssl x509 -req -in ($src | path join $"($cluster_name).csr") -CA ($src | path join "ca.crt") 
      -CAkey ($src | path join "ca.key") -out ($src | path join $"($cluster_name).crt") -days $sign_days 
      -extensions ssl_client -extfile ($src | path join "openssl.conf") 
  ) 
  let res = (^openssl genrsa -passout $"file:($src | path join "pass")" $sign_cipher
      -out ($src | path join $"($hostname)_p.key") $long_sign
  | complete)
  if $res.exit_code != 0 { 
    do $on_error $"openssl genrsa error ($res.exit_code) ($res.stdout)"
    exit 1
  }
  ^openssl rsa -in ($src | path join $"($hostname)_p.key") -out ($src | path join $"($hostname).key")
  if not ($src | path join  "openssl.conf" | path exists) { 
    print $"openssl.con not found in ($src | path join "openssl.conf") "
    rm -f ($src | path join "pass") 
    exit 1
  }
  let res = (^openssl req -newkey rsa:$long_sign -passout $"file:($src | path join "pass")" 
    -key ($src | path join $"($hostname).key") -out ($src | path join $"($hostname).csr")
    -subj $"/CN=($etcd_cn)" -config ($src | path join "openssl.conf") -extensions ssl_peer
  | complete)
  if $res.exit_code == 0 { 
    do $on_error $"openssl req key error ($res.exit_code) ($res.stdout)"
    exit 1
  }
  let res = (^openssl x509 -req -in ($src | path join $"($hostname).csr") -CA ($src | path join "ca.crt") -CAkey ($src | path join "ca.key")
    -out ($src | path join $"($hostname).crt") -days $sign_days
    -extensions ssl_peer -extfile ($src | path join "openssl.conf")
  | complete)
  if $res.exit_code != 0 { 
    do $on_error $"openssl x509 req cst error ($res.exit_code) ($res.stdout)"
    exit 1
  } 
  rm -f ($src | path join "pass")
}

def openssl_mode [ 
  defs: record
  src: string
  run_root: string
  cluster_name: string
  hostname: string
  signature: string
  long_sign: int
] {
  let etcd_cn = ( $defs.taskserv.cn | default "")
  let ca_signature = ($defs.taskserv.ca_sign | default "")
  let ssl_curve = ($defs.taskserv.ssl_curve | default "")
  let sign_sha = ($defs.taskserv.sign_sha | default "")
  let sign_cipher = ($defs.taskserv.cipher | default "")
  let sign_days = ($defs.taskserv.sign_days | default "")
  let ca_sign_days = ($defs.taskserv.ca_sign_days | default "")

  mut openssl = (^bash -c "type -P openssl")
  if $openssl == "" { 
    ^sudo apt install openssl -y 
    $openssl = (^bash -c "type -P openssl")
  }
  if openssl == "" { print $"openssl not installed " ; exit 1 }
  if  not ($src | path join "openssl.conf" | path exists) and ($run_root | path join "openssl.conf.tpl" | path exists) {
    cp ($run_root | path join "openssl.conf.tpl")  ($src | path join "openssl.conf")
    if ($src | path join "openssl_conf_alt_names" | path exists ) { 
      open ($src | path join "openssl_conf_alt_names") -r | save -a ($src | path join "openssl.conf") 
    }
  } 
  print $"CA signature: ($ca_signature)"
  if not ($src | path join "ca.key" | path exists) {
    sops_cmd "decrypt" ($src | path join "ca.key") ($src | path join "ca.key") --error_exit
    #sudo mv "$src/ca.key.$$" "$src/ca.key"
  } 
  if $ca_signature == "ECC" {
    if not ($src | path join "ca.key" | path exists) and not ($src| path join "ca.crt" | path exists)  {
      ^openssl ecparam -genkey -name $ssl_curve -out ($src | path join "ca.key")
      let res = (^openssl req -x509 -extensions v3_ca -config ($src | path join "openssl.conf") -new $"-SHA($sign_sha)"
        -nodes -key ($src  | path join "ca.key") -days $ca_sign_days 
        -out ($src | path join "ca.crt") -subj $"/CN=($etcd_cn)"
      | complete )
      if $res.exit_code != 0 { 
        print $"🛑 openssl key ($ca_signature) error ($res.stdout)"  
        exit 1
      } 
    } 
  } else if not ($src | path join "ca.key" | path exists) and not ($src |path join "ca.crt" | path exists) {
    $defs.taskserv.sign_pass | save -f ($src | path join "pass")
    ^openssl genrsa -passout $"file:($src | path join "pass")" $sign_cipher -out ($src | path join "ca_p.key") $long_sign
    ^openssl rsa -in ($src |path join "ca_p.key")  -out ($src | path join "ca.key")
    let res = (^openssl req -x509 -extensions v3_ca -config ($src | path join "openssl.conf") -newkey rsa:($long_sign) 
      -nodes -key ($src | path join "ca.key") -days $sign_days -out ($src | path join "ca.crt") -subj $"CN=($etcd_cn)" 
    | complete )
    if $res.exit_code != 0 { 
      print $"🛑 openssl ca ($ca_signature) error ($res.stdout)"  
      exit 1
    } 
  } 
  print $"Certs signature: ($signature)"
  if not ($src | path join $"($cluster_name).crt" | path exists) or not ($src | path join $"($cluster_name).key" | path exists)  {
    match $signature { 
      "ECC" => { 
        (openssl_ecc_cert $defs $src $run_root $cluster_name $hostname $signature $long_sign)
      },
    _ => {
        (openssl_rsa_cert $defs $src $run_root $cluster_name $hostname $signature $long_sign)
      }, 
    }
  }
  copy_certs $defs $src $run_root $cluster_name $signature 
}
def cfssl_mode [ 
  defs: record
  src: string
  run_root: string
  cluster_name: string
  hostname: string
  signature: string
  long_sign: int
] {
  let domain_name = (get_domain_name $defs ($defs.taskserv.domain_name | default ""))
  let source_name = $"($cluster_name | default "").($domain_name)"
  let ORG = $env.PWD 
  let etcd_c = ($defs.taskserv.c | default "") 

  mut CFSSL = (^bash -c "type -P cfssl")
  if "$CFSSL" == "" {  
    let cfssl_install_bin = ($env.PROVISIONING | path join "core"| path join "bin" | path join "cfssl-install.sh")
    if ($cfssl_install_bin | path exists) { ^$cfssl_install_bin } 
    $CFSSL = (^bash -c "type -P cfssl")
  }
  if "$CFSSL" == "" { print $"cfssl not installed " ; exit 1 }
  let CFSSLJSON = (^bash -c "type -P cfssljson")
  let csr_json_file = ($src | path join "csr.json") 
  if not ($csr_json_file) { 
    "{" | tee { save -f $csr_json_file }  | ignore
    $"\"hosts\": [" | tee { save -a $csr_json_file } | ignore
    for server in $defs.defs.servers {
      let ip = ($server.network_private_ip | default "")
      if $ip == "" { continue }
      $"\"($server.hostname)\",\"($server.hostname).($domain_name)\",\"($ip)\"," | tee { save -a $csr_json_file } | ignore
    }
    if $source_name != "" and $source_name != $"($cluster_name).($domain_name)" { 
      print $"\"($source_name)\","| tee { save -a ($src | path join "csr.json") } | ignore
    }
    $"\"${domain_name}\", \"$cluster_name\"],\"key\": {" | tee { save -a $csr_json_file } | ignore
    if $signature == "ECC" {
      $"\"algo\": \"ecdsa\",\"size\": ($long_sign) " | tee { save -a $csr_json_file } | ignore
    } else { 
      $"\"algo\": \"rsa\",\"size\": ($long_sign) " | tee { save -a $csr_json_file } | ignore
    }
    $"}, \"names\": [{ \"C\":\"($etcd_c)\", \"CN\": \"($domain_name)\" }]" | tee { save -a $csr_json_file } | ignore
    $"}" | tee { save -a $csr_json_file }  | ignore
    #sudo echo '{"CN":"CA","key":{"algo":"rsa","size":2048}}' | cfssl gencert -initca - | cfssljson -bare ca -
    #$sudo echo '{"signing":{"default":{"expiry":"43800h","usages":["signing","key encipherment","server auth","client auth"]}}}' \&ca-config.json
  }
  if not ( $"($cluster_name).key" | path exists) {
    cd $src
    if ((^($CFSSL) genkey -initca csr.json | ^($CFSSLJSON) -bare ca) | complete).exit_code == 0 {
      if ((^($CFSSL) gencert -ca ca.pem -ca-key ca-key.pem csr.json  
        | ^($CFSSLJSON) -bare $cluster_name) | complete).exit_code == 0 {
        mv ca.pem ca.crt
        sudo mv ca-key.pem ca.key
        mv $"($cluster_name).pem" $"($cluster_name).crt"
        sudo mv $"($cluster_name)-key.pem" $"($cluster_name).key"
        for server in $defs.defs.servers {
          cp $"($cluster_name).crt" $"($server.hostname).crt"
          sudo cp $"($cluster_name).key" $"($server.hostname).key"
        }
        cd $ORG 
        copy_certs $defs $src $run_root $cluster_name $signature
      }
    }
    cd $ORG 
  } else {
    copy_certs $defs $src $run_root $cluster_name $signature
  }
}

export def make_certs [
  defs: record
  src: string
  run_root: string
  cluster_name: string
  signature: string
  ssl_mode: string 
  settings_root: string
  long_sign: int
] {
  if $signature == "" { print $"No signatures found" ; return 1 }
  if not ($src | path exists) { print $"Directory ($src) not found" ; return 1 }
  let hostname = ($defs.server.hostname | default "")
  if $hostname == "" { print $"hostname not found in ($env.PROVISIONING_VARS)" ; exit 1 }
  let servers_list = ($defs.defs.servers | select "hostname"  | flatten | get -i "hostname") 
  match $ssl_mode {
    "open" | "openssl" => {
      openssl_mode $defs $src $run_root $cluster_name $hostname $signature $long_sign
    },
    "cf" | "cfssl" => { 
      cfssl_mode $defs $src $run_root $cluster_name $hostname $signature $long_sign
    },
  }
}
export def etcd_conf [
  defs: record
  src: string
  run_root: string
  cluster_name: string
  signature: string
  ssl_mode: string
] {
  if not ($src | path exists) { mkdir $src }
  let domain_name = (get_domain_name $defs ($defs.taskserv.domain_name | default ""))
  let etcd_cn = ( $defs.taskserv.cn | default "")
  let source_name = $"($cluster_name | default "").($domain_name)"
  if $domain_name ==  "" or $domain_name == "" { print $"No names \( cluster_name and domain \) are defined" ; return 1 }
  if $env.PROVISIONING_DEBUG { print $"nodeport: ($defs.taskserv.peer_port) \nprotocol: ($defs.taskserv.etcd_protocol) \n" }
  let conf_alt_names_path = ($src | path join "openssl_conf_alt_names")
  let setup_tpl_path = ($src | path join "setup.tpl")
  mut n = 0
  match $ssl_mode { 
    "open"| "openssl" => {
      rm -f $conf_alt_names_path $setup_tpl_path 
      if $defs.taskserv.use_localhost {
        if $env.PROVISIONING_DEBUG { print $"localhost: 127.0.0.1" } 
        match $ssl_mode  { 
          "open"| "openssl" => {
            $n += 1
            $"DNS.$n = localhost" | tee { save -a $conf_alt_names_path } | ignore
            $"IP.$n = 127.0.0.1" | tee { save -a $conf_alt_names_path } | ignore
          } 
        }      
      } 
      $n += 1
      $"DNS.($n) = ($cluster_name)" | tee { save -a $conf_alt_names_path } | ignore
      $n += 1
      $"DNS.($n) = ($etcd_cn)" | tee { save -a $conf_alt_names_path } | ignore
    }  
  }
  mut cluster_list = ""
  for server in $defs.defs.servers { 
    let ip = ($server.network_private_ip | default "") 
    if $ip == "" { continue }
    if $env.PROVISIONING_DEBUG { print $"($server.hostname): ($ip)" } 
    if $cluster_list != "" { $cluster_list += "," } 
    $cluster_list += $"($server.hostname)=($defs.taskserv.etcd_protocol)://($ip):($defs.taskserv.peer_port)"
    $n += 1
    match $ssl_mode { 
      "open"| "openssl" => {
        $"export Node($n)_IP=($ip)" | tee { save -a $setup_tpl_path } | ignore
        $"DNS.($n) = ($server.hostname)" | tee { save -a $conf_alt_names_path } | ignore
        $"IP.($n) = ($ip)" | tee { save -a $conf_alt_names_path } | ignore
        $n += 1
        $"DNS.($n) = ($server.hostname).($domain_name)" | tee { save -a $conf_alt_names_path } | ignore
      }  
   }
  }
  match $ssl_mode {
    "open"| "openssl" => {
      if $source_name != "" and $source_name != $"($cluster_name).($domain_name)" {
        $n += 1
        print $"DNS.($n) = ($source_name)" | tee { save -a $conf_alt_names_path } | ignore
      } 
    }
  }
  if $env.PROVISIONING_DEBUG { print $"\ncluster_list: ($cluster_list)" }
  return 0
}

export def copy_certs [
  defs: record
  src: string
  run_root: string
  cluster_name: string
  signature: string
] { 
  print $"Copy certs to ($run_root) ..."
  let hostname = $defs.server.hostname
  if $hostname == "" { print $"hostname not found for ($env.PROVISIONING_VARS)" ; exit 1 } 
  if (glob ($src | path join "*.csr") | length) > 0 {
    rm -f ...(glob ($src | path join "*.csr")) 
  }
  if not ($run_root | path join "certs" | path exists) { mkdir ($run_root | path join "certs") }
  for name in [ ca $hostname $cluster_name] { 
    if not ($src | path join $"($name).key" | path exists) { continue }
    if (sops_cmd "is_sops" ($src | path join $"($name).key")) { 
      let content = (sops_cmd "decrypt" ($src | path join $"($name).key") --error_exit)
      if $content != "" { $content | save -f ($run_root | path join "certs" | path join $"($name).key") } 
    } else {
      cp ($src | path join $"($name).key") ($run_root | path join "certs" | path join $"($name).key" )
      sops_cmd "encrypt" ($src | path join $"($name).key") --error_exit | save -f ($src | path join $"($name).key")
    }
    chmod 400 ($src | path join $"($name).key") ($run_root | path join "certs" | path join $"($name).key")
    if ($src | path join $"($name).crt" | path exists) {
      cp ($src | path join $"($name).crt") ($run_root | path join "certs")
    }
  }
  if ($src | path join $"($cluster_name).crt" | path exists) {
    #if not ($run_root | path join "certs" | path join $"($cluster_name).crt" | path exists) {
    #    cp ($src | path join $"($cluster_name).crt") ($run_root | path join "certs")
    #}
    if not ($run_root | path join "certs" | path join $"($hostname).crt" | path exists) {
      cp ($src | path join $"($cluster_name).crt") ($run_root | path join "certs" | path join $"($hostname).crt")
    }
    if not ($run_root | path join "certs" | path join $"($hostname).key" | path exists) {
      cp ($run_root | path join "certs" |  path join $"($cluster_name).key") ($run_root | path join "certs" | path join $"($hostname).key")
    }
    print $"Certificate for ($hostname) signed ($signature) in ($src) copy to deployment" 
  }
  if (glob ($run_root | path join "openssl.*") | length) > 0 {
    rm -r ...(glob ($run_root | path join "openssl.*"))
  }
}

def main [] { 

  print $"(_ansi green_bold)ETCD(_ansi reset) with ($env.PROVISIONING_VARS?) " 
  let run_root = $env.PROVISIONING_WK_ENV_PATH

  let defs = load_defs
  let src = ($env.PROVISIONING_SETTINGS_SRC_PATH | path join "resources" | path join $defs.taskserv.prov_path)
  if not ($env.PROVISIONING_SETTINGS_SRC_PATH | path join "resources" | path exists) {
    ^mkdir -p ($env.PROVISIONING_SETTINGS_SRC_PATH | path join "resources")
  }
  let provision_path = ($defs.taskserv.prov_path | default "" | str replace "~" $env.HOME)
  if $provision_path == "" { 
    print $"🛑 prov_path not found taskserv definition"  
    exit 1
  }
  let cluster_name = $defs.taskserv.cluster_name | default ""
  if $cluster_name == "" { 
    print $"🛑 cluster_name not foundi taskserv definition"  
    exit 1
  }
  let domain_name = (get_domain_name $defs ($defs.taskserv.domain_name | default ""))
  if $domain_name == "" { 
    print $"🛑 domain_name nor found in settings"  
    exit 1
  }

  let source_name = $"($cluster_name | default "").($domain_name)"

  let settings_root = ($env.PROVISIONING_SETTINGS_SRC_PATH | default "" )
  let signature = ($defs.taskserv.ssl_sign | default "")
  let ssl_mode = ($defs.taskserv.ssl_mode | default "")
  let long_sign = ($defs.taskserv.long_sign | default 0)

  if ($env.PROVISIONING_SETTINGS_SRC_PATH | path join $provision_path | path join $"($cluster_name).crt" | path exists) { 
    copy_certs $defs $src $run_root $cluster_name $signature
  } else {
    if not ($env.PROVISIONING_SETTINGS_SRC_PATH | path join $provision_path | path exists) { 
      ^mkdir -p ($env.PROVISIONING_SETTINGS_SRC_PATH | path join $provision_path) 
    }
    etcd_conf $defs $src $run_root $cluster_name $signature $ssl_mode 
    make_certs $defs $src $run_root $cluster_name $signature $ssl_mode $settings_root $long_sign
  }
}
