{%- set parameter_dict = dict(default_parameter_dict, **parameter_dict) %}
{%- set additional_frontend = parameter_dict['additional-frontend-guid'] %}
{%- set embedded_instance_config = parameter_dict['initial-embedded-instance'] %}

[buildout]
extends =
  ${monitor-template:output}

theia-environment-parts =
  tasks.json
  slapos-repository
  runner-link
  settings.json

theia-parts =
  frontend-reload
  promises

parts =
  monitor-base
  $${:theia-parts}
  $${:theia-environment-parts}
  publish-connection-parameter

eggs-directory = ${buildout:eggs-directory}
develop-eggs-directory = ${buildout:develop-eggs-directory}
offline = true


[publish-connection-parameter]
<= monitor-publish
recipe = slapos.cookbook:publish
url = $${remote-frontend:connection-secure_access}
{% if additional_frontend %}
additional-url = $${remote-additional-frontend:connection-secure_access}
{% endif %}
username = $${frontend-instance-password:username}
password = $${frontend-instance-password:passwd}
backend-url = $${frontend-instance:url}
ipv6 = {{ ipv6_random }}

[directory]
recipe = slapos.cookbook:mkdirectory
home = $${buildout:directory}
etc = $${:home}/etc
var = $${:home}/var
srv = $${:home}/srv
bin = $${:home}/bin
tmp = $${:home}/tmp
dot-theia = $${:home}/.theia/
pidfiles = $${:var}/run

services = $${:etc}/service
runner = $${:srv}/runner
backup = $${:srv}/backup/theia
project = $${:srv}/project

frontend-static = $${:srv}/frontend-static
frontend-static-public = $${:frontend-static}/public
frontend-static-css = $${:frontend-static}/css

bash-completions = $${:home}/.local/share/bash-completion/completions/
fish-completions = $${:home}/.config/fish/completions/


# Monitor
# -------

[monitor-instance-parameter]
monitor-httpd-port = {{ parameter_dict['monitor-httpd-port'] }}
{%- for k in ('monitor-cors-domains', 'monitor-username', 'monitor-password') %}
{%-   set v = parameter_dict.get(k) %}
{%-   if v %}
{{ k[8:] }} = {{ v }}
{%-   endif %}
{%- endfor %}
{%- for k in ('monitor-url-list', ) %}
{%-   set v = parameter_dict.get(k) %}
{%-   if v %}
{{ k }} = {{ v }}
{%-   endif %}
{%- endfor %}


# Promises
# --------

[promises]
recipe =
instance-promises =
  $${theia-listen-promise:name}
  $${frontend-listen-promise:name}
  $${frontend-authentication-promise:name}
  $${remote-frontend-url-available-promise:name}
  {% if additional_frontend %}
  $${remote-additional-frontend-url-available-promise:name}
  {% endif %}
  $${slapos-standalone-listen-promise:name}
  $${slapos-standalone-ready-promise:name}
  $${slapos-autorun-promise:name}
  {% if embedded_instance_config %}
  $${embedded-instance-requested-promise:name}
  {% endif %}

[theia-listen-promise]
<= monitor-promise-base
promise = check_socket_listening
name = $${:_buildout_section_name_}.py
config-host = $${theia-instance:ip}
config-port = $${theia-instance:port}

[frontend-listen-promise]
<= monitor-promise-base
promise = check_socket_listening
name = $${:_buildout_section_name_}.py
config-host = $${frontend-instance:ip}
config-port = $${frontend-instance:port}

[frontend-authentication-promise]
<= monitor-promise-base
promise = check_url_available
name = $${:_buildout_section_name_}.py
ip = $${frontend-instance:ip}
port = $${frontend-instance:port}
config-url = https://[$${:ip}]:$${:port}
config-username = $${frontend-instance-password:username}
config-password = $${frontend-instance-password:passwd}

[remote-frontend-url-available-promise]
<= monitor-promise-base
promise = check_url_available
name = $${:_buildout_section_name_}.py
config-url = $${remote-frontend:connection-secure_access}
config-http-code = 401

{% if additional_frontend %}
[remote-additional-frontend-url-available-promise]
<= monitor-promise-base
promise = check_url_available
name = $${:_buildout_section_name_}.py
config-url = $${remote-additional-frontend:connection-secure_access}
config-http-code = 401
{% endif %}

[slapos-standalone-listen-promise]
<= monitor-promise-base
promise = check_socket_listening
# XXX promise plugins can not contain "slapos" in their names
name = standalone-listen-promise.py
config-host = $${slapos-standalone-instance:hostname}
config-port = $${slapos-standalone-instance:port}

[slapos-standalone-ready-promise]
<= monitor-promise-base
promise = check_socket_listening
name = standalone-ready-promise.py
config-abstract = $${slapos-standalone-config:abstract-socket-path}

[slapos-autorun-promise]
<= monitor-promise-base
promise = check_service_state
name = autorun-state-promise.py
config-service = $${slapos-autorun:service-name}
config-expect = $${slapos-autorun:autorun}
config-run-directory = $${directory:runner}/var/run

{% if embedded_instance_config %}
[embedded-instance-requested-promise]
<= monitor-promise-base
promise = check_command_execute
name = embedded-instance-requested-promise.py
config-command = $${embedded-instance-requested-promise-script:output}
{% endif %}


# Remote Caddy Frontend
# ---------------------

[remote-frontend-base]
<= slap-connection
recipe = slapos.cookbook:requestoptional
shared = true
config-url = $${frontend-instance:url}
config-https-only = true
config-type = websocket
config-websocket-path-list = /services /socket.io
return = domain secure_access

[remote-frontend]
<= remote-frontend-base
name = {{ parameter_dict['frontend-name'] }}
software-url = {{ parameter_dict['frontend-sr'] }}
software-type = {{ parameter_dict['frontend-sr-type'] }}
{%- if parameter_dict.get('frontend-guid') %}
sla-instance_guid = {{ parameter_dict['frontend-guid'] }}
{%- endif %}

{% if additional_frontend %}
[remote-additional-frontend]
<= remote-frontend-base
name = {{ parameter_dict['additional-frontend-name'] }}
software-url = {{ parameter_dict['additional-frontend-sr'] }}
software-type = {{ parameter_dict['additional-frontend-sr-type'] }}
{%- if parameter_dict.get('additional-frontend-guid') %}
sla-instance_guid = {{ parameter_dict['additional-frontend-guid'] }}
{%- endif %}
{% endif %}


# Local Caddy Frontend
# --------------------

[frontend-instance-password]
recipe = slapos.cookbook:generate.password
username = admin
storage-path = $${buildout:parts-directory}/.$${:_buildout_section_name_}

[frontend-instance-port]
recipe = slapos.cookbook:free_port
minimum = 3000
maximum = 3100
ip = {{ ipv6_random }}

[frontend-instance-certificate]
recipe = plone.recipe.command
command =
  if [ ! -e $${:key-file} ]
  then
    ${openssl-output:openssl} req -x509 -nodes -days 3650 \
      -subj "/C=AA/ST=X/L=X/O=Dis/CN=$${:common-name}" \
      -newkey rsa:1024 -keyout $${:key-file} \
      -out $${:cert-file}
  fi
update-command = $${:command}
key-file = $${directory:etc}/$${:_buildout_section_name_}.key
cert-file = $${directory:etc}/$${:_buildout_section_name_}.crt
common-name = $${frontend-instance-config:ip}
location =
  $${:key-file}
  $${:cert-file}

[frontend-instance-config]
recipe = slapos.recipe.template
output = $${directory:etc}/$${:_buildout_section_name_}
inline =
  :$${:port} {
    bind $${:ip}
    tls $${frontend-instance-certificate:cert-file} $${frontend-instance-certificate:key-file}
    log stdout
    errors stderr
    gzip
    # because caddy does not support upgrade http2 to websocket
    # https://tools.ietf.org/html/rfc8441
    tls {
      alpn http/1.1
    }
    root $${directory:frontend-static}
    browse
    proxy / $${theia-instance:base-url} {
      except $${frontend-instance-fonts:folder-name} $${frontend-instance-slapos.css:folder-name} public $${favicon.ico:filename} $${frontend-instance-logo:filename}
    }
    proxy /services $${theia-instance:base-url} {
      websocket
    }
    proxy /socket.io $${theia-instance:base-url} {
      websocket
    }
    basicauth $${frontend-instance-password:username} $${frontend-instance-password:passwd} {
      realm "Theia"
      /
    }
  }
ip = $${frontend-instance-port:ip}
hostname = [$${:ip}]
port = $${frontend-instance-port:port}

[frontend-instance]
recipe = slapos.cookbook:wrapper
wrapper-path = $${directory:services}/$${:_buildout_section_name_}
command-line =
  ${caddy:output} -conf $${frontend-instance-config:output} -pidfile $${:pidfile}

ip = $${frontend-instance-config:ip}
hostname = $${frontend-instance-config:hostname}
port = $${frontend-instance-config:port}
pidfile = $${directory:pidfiles}/$${:_buildout_section_name_}.pid
url = https://$${:hostname}:$${:port}/

[frontend-instance-fonts]
; XXX caddy 1 does not seem to serve different folders at different locations
; so we link fonts in static folder
recipe = plone.recipe.command
location = $${directory:frontend-static}/$${:folder-name}
folder-name = fonts
command =
  mkdir -p $${:location}
  ln -sf ${source-code-pro-fonts:location} $${:location}/source-code-pro
  ln -sf ${jetbrains-mono-fonts:location} $${:location}/jetbrains-mono
stop-on-error = true

[frontend-instance-logo]
recipe = plone.recipe.command
filename = logo.png
full-path = $${directory:frontend-static}/$${:filename}
command =
  cp --remove-destination ${logo.png:output} $${:full-path}
stop-on-error = true

[frontend-instance-slapos.css]
recipe = slapos.recipe.template:jinja2
url = ${slapos.css.in:output}
output = $${directory:frontend-static}/$${:folder-name}/slapos.css
folder-name = css
context =
  key logo_image frontend-instance-logo:filename

[frontend-reload]
recipe = slapos.cookbook:wrapper
wrapper-path = $${directory:services}/$${:_buildout_section_name_}
command-line =
  ${bash:location}/bin/bash -c
  "kill -s USR1 $$(${coreutils:location}/bin/cat $${frontend-instance:pidfile}) \
    && ${coreutils:location}/bin/sleep infinity"
hash-files =
  $${frontend-instance-config:output}
  $${frontend-instance:wrapper-path}
wait-for-files = $${frontend-instance:pidfile}

[favicon.ico]
# generate a pseudo random favicon, different for each instance name.
recipe = slapos.recipe.build
seed = {{ root_title }}
install =
  import hashlib, shutil
  buildout_offline = self.buildout['buildout']['offline']
  self.buildout['buildout']['offline'] = 'false'
  try:
    gravatar_url = "https://www.gravatar.com/avatar/" + hashlib.md5(
      options['seed'].encode()
    ).hexdigest() + "?s=256&d=retro"
    shutil.copy(self.download(gravatar_url), location)
  except Exception:
    # Because installation should work offline, if we can't download a favicon,
    # just ignore this step.
    self.logger.exception("Error while downloading favicon, using empty one")
    open(location, 'w').close()
  finally:
    self.buildout['buildout']['offline'] = buildout_offline

location = $${directory:frontend-static}/$${:filename}
filename = $${:_buildout_section_name_}


# Common Environment
# ------------------

[common-environment]
recipe = slapos.recipe.template
output = $${directory:bin}/$${:_buildout_section_name_}
inline =
  #!/bin/sh
  export HOME=$${directory:home}
  export PATH=${java-jdk:location}/bin:${cli-utilities:PATH}:$HOME/.cargo/bin:$PATH
  export IPV6_SLAPRUNNER={{ ipv6_random }}

# Theia Backend
# -------------

[theia-service-port]
recipe = slapos.cookbook:free_port
minimum = 3500
maximum = 3600
ip = {{ ipv4_random }}

[theia-service]
recipe = slapos.recipe.template
output = $${directory:bin}/$${:_buildout_section_name_}
inline =
  #!/bin/sh
  {% raw -%}
  export THEIA_WEBVIEW_EXTERNAL_ENDPOINT='{{hostname}}'
  export THEIA_MINI_BROWSER_HOST_PATTERN='{{hostname}}'
  {% endraw -%}
  export THEIA_OPEN_EDITOR_TOKEN=$(${openssl:location}/bin/openssl rand -hex 32)
  export THEIA_URL=$${:base-url}
  export THEIA_SHELL=$${theia-shell:output}
  export TMP=$${directory:tmp}
  export TEMP=$TMP
  export LC_ALL=C.UTF-8
  export TERMINFO=${ncurses:location}/lib/terminfo/
  export EDITOR="${theia-open:output} --wait"
  export THEIA_DEFAULT_PLUGINS="local-dir:${theia-plugins:location}"
  . $${common-environment:output}
  exec ${theia-wrapper:output} "$@"
ip =  $${theia-service-port:ip}
port = $${theia-service-port:port}
base-url = http://$${:ip}:$${:port}/

[theia-instance]
recipe = slapos.cookbook:wrapper
wrapper-path = $${directory:services}/$${:_buildout_section_name_}
command-line = $${theia-service:output}  --hostname=$${:hostname} --port=$${:port} $${directory:project}
hash-existing-files =
  ${yarn.lock:target}
  ${theia-wrapper:output}
ip =  {{ ipv4_random }}
hostname = $${:ip}
port = $${theia-service:port}
base-url = $${theia-service:base-url}

[theia-shell]
recipe = slapos.recipe.template:jinja2
output = $${directory:bin}/$${:_buildout_section_name_}
inline =
  {% raw -%}
  #!{{ bash }}
  SHELL=$BASH
  # when running interactively, activate slapos configuration and reset GIT_EXEC_PATH to workaround https://github.com/eclipse-theia/theia/issues/7555
  if [ $# = 0 ]; then
    . {{ activate }}
    unset GIT_EXEC_PATH
    set -- --rcfile {{ bashrc }}
  fi
  exec "$SHELL" "$@"
  {% endraw %}
context =
  raw bash ${bash:location}/bin/bash
  key activate slapos-standalone-activate:output
  key bashrc theia-bashrc:output

[theia-bashrc]
recipe = slapos.recipe.template
output = $${directory:etc}/$${:_buildout_section_name_}
inline =
  # enable bash completion
  . ${bash-completion:location}/etc/profile.d/bash_completion.sh
  # source user's .bashrc
  [ -f ~/.bashrc ] && . ~/.bashrc
depends =
  $${shell-setup-completion:recipe}

[shell-setup-completion]
recipe = plone.recipe.command
stop-on-error = true
command =
  ${buildout:bin-directory}/slapos complete > $${directory:bash-completions}/slapos
  ${buildout:bin-directory}/slapos complete --shell fish > $${directory:fish-completions}/slapos.fish


# Embedded Instance
# -----------------

[embedded-instance-config]
recipe = slapos.recipe.template:jinja2
output = $${directory:etc}/$${:_buildout_section_name_}.json
once = $${:output}
config = {{ dumps(embedded_instance_config) }}
context =
  key config :config
inline =
{%-     raw %}
  {{ config or "{}"}}
{%-     endraw %}

[embedded-instance-requested-promise-script]
recipe = slapos.recipe.template:jinja2
output = $${directory:bin}/$${:_buildout_section_name_}
exitcode-file = $${slapos-standalone-script:embedded-request-exitcode-file}
context =
  key exitcodefile :exitcode-file
{%- raw %}
inline =
  #!/bin/sh
  if ! [ -f {{ repr(exitcodefile) }} ]
  then
    echo "ERROR embedded_instance has not been requested"
    exit 1
  elif [ "$(cat {{ repr(exitcodefile) }})" = 0 ]
  then
    echo "OK embedded_instance has been sucessfully requested"
    exit 0
  else
    echo "ERROR request of embedded_instance failed"
    exit 1
  fi
{%- endraw %}


# SlapOS Standalone
# -----------------

[slapos-standalone-port]
recipe = slapos.cookbook:free_port
minimum = 4000
maximum = 4100
ip = {{ ipv4_random }}

[slapos-standalone-config]
ipv4 = {{ ipv4_random }}
ipv6 = {{ ipv6_random }}
port = $${slapos-standalone-port:port}
base-directory = $${directory:runner}
software-root = $${directory:runner}/software
instance-root = $${directory:runner}/instance
local-software-release-root = $${directory:home}
slapos-bin = ${buildout:bin-directory}/slapos
slapos-configuration = $${directory:runner}/etc/slapos.cfg
computer-id = slaprunner
abstract-socket-path = $${directory:home}/standalone-ready

[slapos-standalone-activate]
recipe = slapos.recipe.template
output = $${directory:bin}/$${:_buildout_section_name_}
inline =
  export PATH=${buildout:bin-directory}:$PATH
  export SLAPOS_CONFIGURATION=$${slapos-standalone-config:slapos-configuration}
  export SLAPOS_CLIENT_CONFIGURATION=$SLAPOS_CONFIGURATION
  echo 'Standalone SlapOS for computer `$${slapos-standalone-config:computer-id}` activated'

[slapos-standalone-script]
recipe = slapos.recipe.template:jinja2
output = $${directory:bin}/$${:_buildout_section_name_}
embedded-request-exitcode-file = $${directory:etc}/embedded-request-exitcode
shared-part-list =
  {{ """${buildout:shared-part-list}""" | indent(2) }}
context =
  raw python_for_standalone ${python-for-standalone:executable}
  raw request_script_path $${directory:project}/request-embedded-instance.sh
  raw parameters_file_path $${directory:project}/embedded-instance-parameters.json
  key request_script_template request-script-example:inline
  key shared_part_list :shared-part-list
  key embedded_request_exitcode_file :embedded-request-exitcode-file
  key embedded_instance_config embedded-instance-config:output
  key home_path directory:home
  key forward_frontend_requests :forward-frontend-requests
  section slap_connection slap-connection
  section slapos_standalone_config slapos-standalone-config
forward-frontend-requests = enabled
url = ${slapos-standalone-script:output}

[slapos-standalone]
recipe = slapos.recipe.template
output = $${directory:bin}/$${:_buildout_section_name_}
inline =
  #!/bin/sh
  . $${common-environment:output}
  #XXX find out where the extra nodejs in theia's PATH comes from
  export PATH=${nodejs:location}/bin/:$PATH
  . $${slapos-standalone-activate:output}
  exec $${slapos-standalone-script:output}

[slapos-standalone-instance]
recipe = slapos.cookbook:wrapper
wrapper-path = $${directory:services}/$${:_buildout_section_name_}
command-line = $${slapos-standalone:output}
hash-files =
  $${slapos-standalone:output}
  $${slapos-standalone-script:output}
hostname = $${slapos-standalone-config:ipv4}
port = $${slapos-standalone-config:port}


# Slapos Standalone Autoprocessing
# --------------------------------

[slapos-autorun]
recipe = plone.recipe.command
command =
  case $${:autorun} in
    ( running ) ${buildout:bin-directory}/supervisorctl -c $${:supervisor-conf} start $${:service-name};;
    ( stopped ) ${buildout:bin-directory}/supervisorctl -c $${:supervisor-conf} stop $${:service-name};;
  esac
update-command = $${:command}
service-name = slapos-node-auto
supervisor-conf = $${directory:runner}/etc/supervisord.conf
autorun = {{ parameter_dict['autorun'] }}


# Theia Local Environment Setup
# -----------------------------

[tasks.json]
recipe = slapos.recipe.template
output = $${directory:dot-theia}/tasks.json
inline =
  {
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
      {
        "label": "slapos node software",
        "detail": "Build all software supplied to the node",
        "type": "shell",
        "command": "${buildout:bin-directory}/slapos",
        "args": [
          "node",
          "software",
          // debug mode can be enabled by commenting out this line:
          // "--buildout-debug",
          "--all"
        ],
        "options": {
          "env": {
            "SLAPOS_CONFIGURATION": "$${slapos-standalone-config:slapos-configuration}",
            "GIT_EXEC_PATH": ""
          }
        },
        "group": {
          "kind": "build",
          "isDefault": true
        },
        "problemMatcher": []
      },
      {
        "label": "slapos node instance",
        "detail": "Create all instances requested on the node",
        "type": "shell",
        "command": "${buildout:bin-directory}/slapos",
        "args": [
          "node",
          "instance",
          // debug mode can be enabled by commenting out this line:
          // "--buildout-debug",
          "--all"
        ],
        "options": {
          "env": {
            "SLAPOS_CONFIGURATION": "$${slapos-standalone-config:slapos-configuration}",
            "GIT_EXEC_PATH": ""
          }
        },
        "problemMatcher": [],
        "group": {
          "kind": "build",
          "isDefault": true
        }
      }
    ]
  }

[slapos-repository]
recipe = slapos.recipe.build:gitclone
repository = https://lab.nexedi.com/nexedi/slapos.git
location = $${directory:project}/slapos
branch = 1.0
develop = true
git-executable = ${git:location}/bin/git

[settings.json]
recipe = slapos.recipe.template:jinja2
output = $${directory:dot-theia}$${:_buildout_section_name_}
once = $${:output}
inline =
  {
    "files.watcherExclude": {
      "**/.eggs/**": true,
      "**/.env/**": true,
      "**/.git/**": true,
      "**/node_modules/**": true,
      "$${directory:runner}/**":true,
      "$${directory:project}/runner/**":true
    },
    "git.terminalAuthentication": false,
    "security.workspace.trust.startupPrompt": "once",
    "zc-buildout.python.executable": "${python-for-buildout-languageserver:executable}"
  }

[runner-link]
recipe = slapos.cookbook:symbolic.link
target-directory = $${directory:project}
link-binary = $${directory:runner}

[request-script-example]
recipe = slapos.recipe.template:jinja2
output = $${directory:project}/$${:_buildout_section_name_}.sh
software_url = ~/srv/project/slapos/software/html5as-base/software.cfg
request_options = html5as-1 $${:software_url}
header_text =
  # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
  # This template is generated automatically by buildout. #
  # Any changes to this file may be overwritten.          #
  # Copy and adapt it to create your own request script.  #
  # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
context =
  key software_url :software_url
  key request_options :request_options
  key header_text :header_text
inline =
{%- raw %}
  #!/bin/sh -e

  {{ header_text }}

  # slapos supply <url> <node> registers a software for installation on a node.
  #
  # A software is uniquely identified by its URL (the URL may be a local path).
  # You may choose from softwares available in ~/srv/project/slapos/software.
  #
  # The one and only SlapOS Node embedded inside Theia is called 'slaprunner'.
  #
  # For more information, run:
  #     slapos help supply
  #
  slapos supply {{ software_url }} slaprunner

  # slapos request <name> <url> registers an instance for allocation on a node.
  #
  # An instance is uniquely identified by its name (and its requester).
  #
  # It will be allocated on a node where the software URL is already supplied.
  # Inside Theia that node can only be 'slaprunner'.
  #
  # For more information, run:
  #     slapos help request
  #
  slapos request {{ request_options }}
{%  endraw %}
