{%- 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 python-enable-user-pip theia-parts = frontend-instance 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} $${python-server-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} [python-server-listen-promise] <= monitor-promise-base promise = check_socket_listening name = $${:_buildout_section_name_}.py config-pathname = $${python-server:socket} [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 Haproxy 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 $${:cert-file} ] then ${openssl-output:openssl} req -x509 -nodes -days 3650 \ -subj "/C=AA/ST=X/L=X/O=Dis/CN=$${:common-name}" \ -newkey rsa:2048 -keyout $${:cert-file} \ -out $${:cert-file} fi update-command = $${:command} cert-file = $${directory:etc}/$${:_buildout_section_name_}.pem common-name = $${frontend-instance-config:ip} location = $${:cert-file} [frontend-instance-config] recipe = slapos.recipe.template:jinja2 url = ${stack-haproxy-default-backend-config:target} output = $${directory:etc}/$${:_buildout_section_name_} context = key pidfile frontend-instance:pidfile key content :content content = userlist basic-auth-list user $${frontend-instance-password:username} insecure-password $${frontend-instance-password:passwd} frontend app log global bind $${:ip}:$${:port} ssl crt $${frontend-instance-certificate:cert-file} alpn h2,http/1.1 # writing twice the same ACL is doing OR acl is_public path_beg /public/ acl is_public path /$${frontend-instance-favicon.ico:filename} acl is_public path /$${frontend-instance-theia.webmanifest:filename} acl is_public path /$${frontend-instance-theia-serviceworker.js:filename} acl auth_ok http_auth(basic-auth-list) # No authentication for public folder http-request auth unless auth_ok || is_public use_backend static if { path_beg /$${frontend-instance-fonts:folder-name} } || { path_beg /$${frontend-instance-slapos.css:folder-name} } || { path /$${frontend-instance-logo:filename} } || is_public default_backend nodejs backend nodejs log global server nodejs_backend $${theia-instance:ip}:$${theia-instance:port} backend static log global server static_backend $${python-server:socket} option forwardfor http-response set-header Content-Security-Policy "default-src 'self'; img-src 'self' data:; script-src 'none'" ip = $${frontend-instance-port:ip} hostname = [$${:ip}] port = $${frontend-instance-port:port} pidfile = $${directory:pidfiles}/haproxy.pid [frontend-instance] recipe = slapos.cookbook:wrapper wrapper-path = $${directory:services}/$${:_buildout_section_name_} command-line = ${haproxy:location}/sbin/haproxy -f $${frontend-instance-config:output} hash-files = $${frontend-instance-config:output} 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 python server only serves one folder ; 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-instance-theia.webmanifest] recipe = slapos.recipe.build short-name = {{ root_title }} name = Theia SlapOS $${:short-name} background-color = #3c3c3c install = import json with open(options['location'], 'w') as f: json.dump({ "name": options["name"], "short_name": options["short-name"], "icons": [ { "src": "/" + self.buildout["frontend-instance-favicon.ico"]["filename"], "sizes": "256x256", "type": "image/png" }, ], "start_url": "/", "display": "fullscreen", "background_color": options["background-color"] }, f) location = $${directory:frontend-static}/$${:filename} filename = theia.webmanifest [frontend-instance-theia-serviceworker.js] recipe = slapos.recipe.template inline = /* minimal service worker for A2HS */ self.addEventListener("fetch", function(event) { }); output = $${directory:frontend-static}/$${:filename} filename = theia-serviceworker.js [frontend-instance-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 = favicon.ico # Local Python Server # ------------------- [python-server] recipe = slapos.recipe.template output = $${directory:services}/$${:_buildout_section_name_} socket = $${directory:run}/$${:_buildout_section_name_}.sock inline = #!$${buildout:executable} import atexit, os, socketserver from http import server class Server(socketserver.ThreadingUnixStreamServer): daemon_threads = True class Handler(server.SimpleHTTPRequestHandler): def address_string(self): # insecure but ok for logging return self.headers.get("X-Forwarded-For", "local") s = "$${:socket}" os.chdir("$${directory:frontend-static}") def cleanup(): try: os.remove(s) except FileNotFoundError: pass atexit.register(cleanup)() Server(s, Handler).serve_forever() # Common Environment # ------------------ [common-environment] recipe = slapos.recipe.template output = $${directory:bin}/$${:_buildout_section_name_} inline = #!/bin/sh export HOME=$${directory:home} export PATH=${cli-utilities:PATH}:$HOME/.cargo/bin:$HOME/.local/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} [theia-shell] recipe = slapos.recipe.template:jinja2 output = $${directory:bin}/$${:_buildout_section_name_} inline = {% raw -%} #!{{ bash }} SHELL=$BASH # when running interactively, or as a login shell, activate slapos configuration # and reset GIT_EXEC_PATH to workaround https://github.com/eclipse-theia/theia/issues/7555 if [ $# = 0 ] || [ $# = 1 -a "$1" = -l ]; 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 # enable color for ls eval "$(${coreutils:location}/bin/dircolors -b)" alias ls='ls --color=auto' # 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 [python-enable-user-pip] # enable pip user installation for python extension recipe = plone.recipe.command stop-on-error = true command = ${python:executable} -m ensurepip --user # 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 = {{ parameter_dict['forward-slapos-frontend-requests'] }} url = ${slapos-standalone-script:output} [slapos-standalone] recipe = slapos.recipe.template output = $${directory:bin}/$${:_buildout_section_name_} inline = #!/bin/sh . $${common-environment:output} . $${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": "$${buildout:directory}/software_release/bin/${python-for-buildout-languageserver:interpreter}" } [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 %}