diff --git a/docker-containers/microservice_jammy/Dockerfile b/docker-containers/microservice_jammy/Dockerfile new file mode 100644 index 00000000..8f32e958 --- /dev/null +++ b/docker-containers/microservice_jammy/Dockerfile @@ -0,0 +1,54 @@ +FROM docker.io/ubuntu:22.04 +MAINTAINER Cerebro + +ENV LANG='C.UTF-8' LC_ALL='C.UTF-8' +ENV DISTRIB_CODENAME=jammy DISTRIB_RELEASE=22.04 +ENV CONFIG_DIR config + +ENV MICROSERVICE_VERSION 2.11.3 + +# Set up container's timezone +RUN DEBIAN_FRONTEND="noninteractive" apt-get update && apt-get install -y tzdata + +#--- Install common utils --- +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y --no-install-recommends \ + apt-utils \ + bash-completion \ + cron \ + curl \ + file \ + gcc \ + git \ + iproute2 \ + less \ + mc \ + nano \ + net-tools \ + netcat \ + python3 \ + python3-dev \ + python3-pip \ + sudo \ + software-properties-common \ + supervisor \ + unzip \ + vim \ + wget \ + haproxy + +#--- Install armada --- +COPY ./armada-microservice_${MICROSERVICE_VERSION}_amd64.deb /tmp/armada-microservice_${MICROSERVICE_VERSION}_amd64.deb +RUN dpkg -i /tmp/armada-microservice_${MICROSERVICE_VERSION}_amd64.deb || true +RUN apt-get install -y --fix-broken --no-install-recommends +RUN dpkg -l armada-microservice + +#--- Cleanup --- +RUN apt-get clean +RUN rm -rf /var/lib/apt/lists/* + +#--- Add python alias --- +RUN ln -s /usr/bin/python3 /usr/bin/python + +CMD ["microservice", "bootstrap"] diff --git a/docker-containers/microservice_jammy/build_package.sh b/docker-containers/microservice_jammy/build_package.sh new file mode 100755 index 00000000..7972b46b --- /dev/null +++ b/docker-containers/microservice_jammy/build_package.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -e +set -x + +SERVICE_NAME=microservice_packaging + +#workdir to file directory +cd "$(dirname "${BASH_SOURCE[0]}")" + +PACKAGE_VERSION=$1 +if [[ ! -n ${PACKAGE_VERSION} ]]; then + PACKAGE_VERSION=$(grep 'ENV MICROSERVICE_VERSION' Dockerfile | awk '{ print $3 }' | tr -d '[[:space:]]') +fi + +docker build --rm -t "${SERVICE_NAME}" -f packaging/Dockerfile ./packaging +docker run --rm -t -v "$(pwd)/packaging:/opt/microservice" "${SERVICE_NAME}" --version="${PACKAGE_VERSION}" + +mv -f "$(pwd)/packaging/armada-microservice_${PACKAGE_VERSION}_amd64.deb" "$(pwd)/" diff --git a/docker-containers/microservice_jammy/packaging/Dockerfile b/docker-containers/microservice_jammy/packaging/Dockerfile new file mode 100644 index 00000000..13ace21a --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/Dockerfile @@ -0,0 +1,30 @@ +FROM docker.io/ubuntu:22.04 + +# Set up container's timezone +RUN DEBIAN_FRONTEND="noninteractive" apt-get update && apt-get install -y tzdata + +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y --no-install-recommends \ + build-essential \ + g++ \ + gcc \ + git \ + libc6-dev \ + libffi-dev \ + make \ + python3 \ + rpm \ + ruby \ + ruby-dev \ + rubygems + +# Install package builder +RUN gem install fpm + +COPY package_build.py /usr/bin/package_build +RUN chmod +x /usr/bin/package_build + +WORKDIR "/opt/microservice" +VOLUME "/opt/microservice" +ENTRYPOINT ["/usr/bin/package_build"] diff --git a/docker-containers/microservice_jammy/packaging/README.md b/docker-containers/microservice_jammy/packaging/README.md new file mode 100644 index 00000000..991aaabc --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/README.md @@ -0,0 +1,5 @@ +# Microservice packaging + +# Building + +```./build_package.sh ``` \ No newline at end of file diff --git a/docker-containers/microservice_jammy/packaging/after-install.sh b/docker-containers/microservice_jammy/packaging/after-install.sh new file mode 100644 index 00000000..56e83bbb --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/after-install.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -ex + +sudo -H python3 -m pip install --upgrade pip setuptools +sudo -H python3 -m pip install --upgrade web.py + +mkdir -p /var/log/supervisor /var/opt/service-registration/ +ln -sf /opt/microservice/microservice /opt/microservice/src + +echo VERSION = \"<%= version %>\" > /opt/microservice/microservice/version.py + +sudo -H python3 -m pip install --upgrade /opt/microservice + +chmod +x /opt/microservice/scripts/* /opt/microservice/src/run_hooks.py + +grep 'source /opt/microservice/scripts/tools/microservice_bashrc.source' /etc/bash.bashrc || { + echo "source /opt/microservice/scripts/tools/microservice_bashrc.source" >> /etc/bash.bashrc +} diff --git a/docker-containers/microservice_jammy/packaging/after-remove.sh b/docker-containers/microservice_jammy/packaging/after-remove.sh new file mode 100644 index 00000000..ab820cf0 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/after-remove.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +sed -i '/source \/opt\/microservice\/scripts\/tools\/microservice_bashrc.source/d' /etc/bash.bashrc diff --git a/docker-containers/microservice_jammy/packaging/microservice/etc/bash_completion_armada/acd b/docker-containers/microservice_jammy/packaging/microservice/etc/bash_completion_armada/acd new file mode 100644 index 00000000..57e2c642 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/etc/bash_completion_armada/acd @@ -0,0 +1,15 @@ +_acd_action() { + + local command="acd" + + local current=${COMP_WORDS[COMP_CWORD]} + local previous=${COMP_WORDS[COMP_CWORD-1]} + + if [[ "$previous" == "$command" ]]; then + COMPREPLY=($(compgen -W "$(find /opt/${IMAGE_NAME}/* -type d -printf "%f\n")" -- $current)) + fi + + return 0 +} + +complete -F _acd_action acd diff --git a/docker-containers/microservice_jammy/packaging/microservice/etc/bash_completion_armada/atail b/docker-containers/microservice_jammy/packaging/microservice/etc/bash_completion_armada/atail new file mode 100644 index 00000000..07269076 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/etc/bash_completion_armada/atail @@ -0,0 +1,15 @@ +_atail_action() { + + local command="atail" + + local current=${COMP_WORDS[COMP_CWORD]} + local previous=${COMP_WORDS[COMP_CWORD-1]} + + if [[ "$previous" == "$command" ]]; then + COMPREPLY=($(compgen -W "$(find /var/log/supervisor/* -printf "%f\n")" -- $current)) + fi + + return 0 +} + +complete -F _atail_action atail diff --git a/docker-containers/microservice_jammy/packaging/microservice/etc/bash_completion_armada/supervisorctl b/docker-containers/microservice_jammy/packaging/microservice/etc/bash_completion_armada/supervisorctl new file mode 100644 index 00000000..43ed8a9a --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/etc/bash_completion_armada/supervisorctl @@ -0,0 +1,23 @@ +_supervisorctl_action() { + + local service_options=(start restart stop status tail pid reload reread update) + local generic_options=(avail maintail shutdown) + local all_options=($(echo ${service_options[@]}) $(echo ${generic_options[@]})) + local command=("supervisorctl" "actl") + + local current=${COMP_WORDS[COMP_CWORD]} + local previous=${COMP_WORDS[COMP_CWORD-1]} + + if [[ "${all_options[@]}" =~ "$current" && "${command[@]}" =~ $previous ]]; then + COMPREPLY=($(compgen -W "$(echo ${all_options[@]})" -- $current) ) + fi + + if [[ "${service_options[@]}" =~ "$previous" ]]; then + COMPREPLY=($(compgen -W "$(supervisorctl status | awk '{print $1}')" -- $current) ) + fi + + return 0 +} + +complete -F _supervisorctl_action supervisorctl +complete -F _supervisorctl_action actl diff --git a/docker-containers/microservice_jammy/packaging/microservice/etc/supervisor/conf.d/armada_agent.conf b/docker-containers/microservice_jammy/packaging/microservice/etc/supervisor/conf.d/armada_agent.conf new file mode 100644 index 00000000..74a51929 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/etc/supervisor/conf.d/armada_agent.conf @@ -0,0 +1,6 @@ +[program:armada_agent] +command=microservice agent +stdout_logfile_maxbytes=5MB +stdout_logfile_backups=2 +stderr_logfile_maxbytes=5MB +stderr_logfile_backups=2 diff --git a/docker-containers/microservice_jammy/packaging/microservice/etc/supervisor/conf.d/local_magellan.conf b/docker-containers/microservice_jammy/packaging/microservice/etc/supervisor/conf.d/local_magellan.conf new file mode 100644 index 00000000..70dc48ab --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/etc/supervisor/conf.d/local_magellan.conf @@ -0,0 +1,3 @@ +[program:local_magellan] +command=microservice local-magellan +exitcodes=0 diff --git a/docker-containers/microservice_jammy/packaging/microservice/etc/supervisor/conf.d/register_in_service_discovery.conf b/docker-containers/microservice_jammy/packaging/microservice/etc/supervisor/conf.d/register_in_service_discovery.conf new file mode 100644 index 00000000..c788ec52 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/etc/supervisor/conf.d/register_in_service_discovery.conf @@ -0,0 +1,4 @@ +[program:register_in_service_discovery] +command=microservice register 80 +startsecs=0 +priority=10 diff --git a/docker-containers/microservice_jammy/packaging/microservice/etc/supervisor/conf.d/require.conf b/docker-containers/microservice_jammy/packaging/microservice/etc/supervisor/conf.d/require.conf new file mode 100644 index 00000000..5dc5a1a5 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/etc/supervisor/conf.d/require.conf @@ -0,0 +1,3 @@ +[program:require] +command=microservice require -c service_discovery.json +startsecs=0 diff --git a/docker-containers/microservice_jammy/packaging/microservice/etc/supervisor/conf.d/supervisord.conf b/docker-containers/microservice_jammy/packaging/microservice/etc/supervisor/conf.d/supervisord.conf new file mode 100644 index 00000000..9ad51d3d --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/etc/supervisor/conf.d/supervisord.conf @@ -0,0 +1,6 @@ +[supervisord] +nodaemon=true +user=root + +[inet_http_server] +port = 127.0.0.1:9001 diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/health-checks/disabled/http-ok b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/health-checks/disabled/http-ok new file mode 100644 index 00000000..1c0cb1fd --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/health-checks/disabled/http-ok @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +body=$(curl -s localhost/health) + +if [ "${body,,}" == "ok" ]; then # ",," is to make ${body} lowercase + echo "HTTP health check OK" + exit 0 +fi +echo "HTTP health check failed" +exit 2 diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/health-checks/main-port-open b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/health-checks/main-port-open new file mode 100644 index 00000000..7944e7e5 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/health-checks/main-port-open @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +check_interface() { + if [[ "${port}" == */udp ]]; then + # -v is necessary, otherwise nc always returns 0 for UDP ports. + nc -z -u -v $1 $(cut -d/ -f1 <<< ${port}) + else + nc -z $1 ${port} + fi +} + +port=80 +if [ -n "$1" ]; then + port="$1" +fi + +ips=$( ip address | awk '/inet/&&!/inet6/{split($2,a,"/");print a[1]}' ) + +for ip in ${ips}; do + check_interface ${ip} + + if [ $? -eq 0 ]; then + exit 0 + fi +done + +exit 2 diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/__init__.py b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/armada_agent.py b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/armada_agent.py new file mode 100644 index 00000000..f517c311 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/armada_agent.py @@ -0,0 +1,384 @@ +import calendar +import glob +import json +import logging +import os +import random +import signal +import subprocess +import sys +import threading +import time +import traceback +from datetime import datetime +from functools import wraps, partial + +import requests +from requests.exceptions import HTTPError + +from microservice.common.consul import consul_query, consul_post, consul_get, consul_put +from microservice.common.docker_client import get_docker_inspect +from microservice.common.service_discovery import register_service_in_armada, register_service_in_armada_v1, \ + UnsupportedArmadaApiException +from microservice.defines import ARMADA_API_URL +from microservice.exceptions import ArmadaApiServiceNotFound +from microservice.register_in_service_discovery import REGISTRATION_DIRECTORY +from microservice.version import VERSION + +HEALTH_CHECKS_PERIOD = 10 +HEALTH_CHECKS_TIMEOUT = 10 +HEALTH_CHECKS_PERIOD_VARIATION = 2 +HEALTH_CHECKS_PERIOD_INCREMENTATION = 1 +HEALTH_CHECKS_PATH_WILDCARD = '/opt/*/health-checks/*' + + +def print_err(*objs): + print(*objs, file=sys.stderr) + + +def print_exc(): + traceback.print_exc() + print_err() + + +def _exists_service(service_id): + try: + return service_id in consul_query('agent/services') + except Exception as e: + logging.exception(e) + return False + + +def _create_tags(): + tag_pairs = [ + ('env', os.environ.get('MICROSERVICE_ENV')), + ('app_id', os.environ.get('MICROSERVICE_APP_ID')), + ] + return ['{k}:{v}'.format(**locals()) for k, v in tag_pairs if v] + + +def _register_service(consul_service_data): + print_err('Registering service...') + response = consul_post('agent/service/register', consul_service_data) + response.raise_for_status() + print_err('Successfully registered.', '\n') + + +def _datetime_string_to_timestamp(datetime_string): + # Converting "2014-12-11T09:24:13.852579969Z" to an epoch timestamp + return calendar.timegm(datetime.strptime( + datetime_string[:-4], "%Y-%m-%dT%H:%M:%S.%f").timetuple()) + + +def _store_start_timestamp(container_id, container_created_timestamp): + key = "kv/start_timestamp/" + container_id + if consul_get(key).status_code == requests.codes.not_found: + response = consul_put(key, str(container_created_timestamp)) + response.raise_for_status() + + +def retry(num_retries, action=None, expected_exception=Exception): + """ + it retries decorated function "num_retries" times + and performs "action" after each "expected_exception" occurrence. + """ + + def decorator(fun): + @wraps(fun) + def wrapper(*args, **kwargs): + counter = 0 + while True: + try: + return fun(*args, **kwargs) + except expected_exception: + if counter >= num_retries: + raise + else: + print_exc() + + if action: + action() + + counter += 1 + + return wrapper + + return decorator + + +@retry(num_retries=float('inf'), action=partial(time.sleep, 0.2)) +def _wait_for_consul(): + agent_self_dict = consul_query('agent/self') + if 'Config' not in agent_self_dict: + raise Exception('Consul not ready yet') + + +def _walk_registration_files(directory): + service_filename = os.environ['MICROSERVICE_NAME'] + files = next(os.walk(directory))[2] + for filename in files: + if filename.startswith(service_filename): + yield os.path.join(directory, filename) + + +def _register_service_from_file(file_path): + with open(file_path) as f: + registration_service_data = json.load(f) + + service_id = registration_service_data['service_id'] + service_name = registration_service_data['service_name'] + service_local_port = registration_service_data['service_container_port'] + service_port = registration_service_data['service_port'] + single_active_instance = registration_service_data['single_active_instance'] + + container_id = service_id.split(':')[0] + docker_inspect = get_docker_inspect(container_id) + container_created_timestamp = _datetime_string_to_timestamp(docker_inspect["Created"]) + + try: + register_service_in_armada_v1(service_id, service_name, service_local_port, os.environ.get('MICROSERVICE_ENV'), + os.environ.get('MICROSERVICE_APP_ID'), container_created_timestamp, + single_active_instance, VERSION) + return + except UnsupportedArmadaApiException: + logging.warning("Armada is using deprecated microservice API. " + "Consider upgrading armada at least to version 2.5.0") + except Exception as e: + logging.exception(e) + service_tags = _create_tags() + register_service_in_armada(service_id, service_name, service_port, service_tags, container_created_timestamp, + single_active_instance) + + +def _register_services(): + num_registered = 0 + for filename in _walk_registration_files(REGISTRATION_DIRECTORY): + _register_service_from_file(filename) + num_registered += 1 + return num_registered + + +def _async_execute_local_command(command): + p = subprocess.Popen( + command, + shell=True, + preexec_fn=os.setsid, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + return p + + +def _get_health_checks_paths(generic_path): + paths = [] + for path in glob.glob(generic_path): + if os.path.isfile(path): + os.chmod(path, 0o755) + paths.append(path) + return paths + + +def _to_health_code(return_code): + if return_code in (0, 1): + return return_code + return 2 + + +def _compute_health_code(return_codes): + if not return_codes: + return 0 + return max(_to_health_code(x) for x in return_codes) + + +def _get_health_status(return_code): + if return_code == 0: + return 'passing' + if return_code == 1: + return 'warning' + return 'critical' + + +def _get_consul_health_endpoint(return_code): + if return_code == 0: + return 'pass' + if return_code == 1: + return 'warn' + return 'fail' + + +# service may be deregistered without our knowledge, +# if we were unsuccessful on reporting health status - that might be the case, +# so let's try to register it again and retry +@retry(num_retries=1, action=_register_services, expected_exception=ArmadaApiServiceNotFound) +def _report_health_status(microservice_id, health_check_code): + try: + _report_health_status_v1(microservice_id, health_check_code) + return + except UnsupportedArmadaApiException: + logging.warning("Armada is using deprecated microservice API. " + "Consider upgrading armada at least to version 2.5.0") + # Support for old armada (<= 2.4.3) version: + try: + endpoint = _get_consul_health_endpoint(health_check_code) + response = consul_put('agent/check/{endpoint}/service:{microservice_id}'.format(**locals())) + response.raise_for_status() + except HTTPError: + raise ArmadaApiServiceNotFound() + + +def _report_health_status_v1(microservice_id, health_check_code): + url = '{}/v1/local/health/{}'.format(ARMADA_API_URL, microservice_id) + r = requests.put(url, json={'health_check_code': health_check_code}) + if r.status_code == 404: + if r.content == 'not found': + raise UnsupportedArmadaApiException() + service_not_found = False + try: + error_json = r.json() + if error_json['error_id'] == 'SERVICE_NOT_FOUND': + service_not_found = True + except Exception: + pass + if service_not_found: + raise ArmadaApiServiceNotFound() + r.raise_for_status() + + +def _terminate_processes(pids): + print_err('At least one health check timed out.') + for pid in pids: + try: + os.killpg(pid, signal.SIGTERM) + except OSError: + pass + + +def _run_health_checks(services_data, timeout): + process_groups = {} + for data in services_data: + process_group = {} + port = data["service_container_port"] + for path in data["paths"]: + command = "{path} {port}".format(**locals()) + process = _async_execute_local_command(command) + process_group[command] = process + if process_group: + process_groups[data["service_id"]] = process_group + else: + print_err('WARNING: No health checks found.') + + pids = [] + for process_group in process_groups.values(): + pids += [p.pid for p in process_group.values()] + + timer = threading.Timer(timeout, _terminate_processes, [pids]) + timer.start() + + for process_group in process_groups.values(): + for command, process in process_group.items(): + process_stdout, process_stderr = process.communicate() + status = _get_health_status(process.returncode) + print_err('health-check command: {command}'.format(**locals())) + print_err('return code: {process.returncode} ({status})'.format(**locals())) + if process_stdout: + print_err('stdout:\n{process_stdout}\n'.format(**locals())) + if process_stderr: + print_err('stderr:\n{process_stderr}\n'.format(**locals())) + print_err() + + timer.cancel() + + health_check_code_dict = {} + for service_id, process_group in process_groups.items(): + return_codes = [process.returncode for process in process_group.values()] + health_check_code = _compute_health_code(return_codes) + health_check_code_dict[service_id] = health_check_code + return health_check_code_dict + + +def _get_health_checks_required_data(): + services_health_checks_data = [] + service_filenames = os.listdir(REGISTRATION_DIRECTORY) + for service_filename in service_filenames: + service_file_path = os.path.join(REGISTRATION_DIRECTORY, service_filename) + with open(service_file_path) as f: + service_health_check_data = json.load(f) + + service_health_check_generic_path = service_health_check_data.get("service_health_check_path", + HEALTH_CHECKS_PATH_WILDCARD) + service_health_check_data["paths"] = _get_health_checks_paths(service_health_check_generic_path) + services_health_checks_data.append(service_health_check_data) + return services_health_checks_data + + +def _service_id_to_service_name(service_id, services_data): + for data in services_data: + if service_id in data.values(): + return data["service_name"] + + +def _get_health_check_period(is_critical): + if not is_critical: + _get_health_check_period.critical_count = 0 + period = HEALTH_CHECKS_PERIOD + random.uniform(-HEALTH_CHECKS_PERIOD_VARIATION, HEALTH_CHECKS_PERIOD_VARIATION) + return period + + try: + _get_health_check_period.critical_count += 1 + except AttributeError: + _get_health_check_period.critical_count = 1 + + incrementation_period = _get_health_check_period.critical_count * HEALTH_CHECKS_PERIOD_INCREMENTATION + + return min(incrementation_period, HEALTH_CHECKS_PERIOD) + + +@retry(num_retries=10, action=partial(time.sleep, 0.2)) +def _retry_register_services(): + """ + Wait for at least one service registration file + """ + num_registered = _register_services() + if not num_registered: + raise Exception('No service registration file found.') + + +def main(): + _wait_for_consul() + _retry_register_services() + + while True: + services_data = _get_health_checks_required_data() + start_time = time.time() + start_datetime = datetime.now().isoformat() + print_err('=== START: {start_datetime} ==='.format(**locals()), '\n') + timeout = HEALTH_CHECKS_TIMEOUT + is_critical = False + + health_check_code_dict = _run_health_checks(services_data, timeout) + for service_id, health_check_code in health_check_code_dict.items(): + service_name = _service_id_to_service_name(service_id, services_data) + status = _get_health_status(health_check_code) + print_err('=== {service_name} STATUS: {status} ==='.format(**locals())) + try: + _report_health_status(service_id, health_check_code) + except Exception as e: + logging.exception(e) + if status == 'critical': + is_critical = True + + period = _get_health_check_period(is_critical) + duration = time.time() - start_time + print_err('\n', 'Health checks took {duration:.2f}s.'.format(**locals())) + if duration < period: + sleep_duration = period - duration + time.sleep(sleep_duration) + print_err() + sys.stdout.flush() + sys.stderr.flush() + + +if __name__ == '__main__': + print('WARNING: Calling this script directly has been deprecated. Try `microservice agent` instead.', + file=sys.stderr) + main() diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/bootstrap_microservice.py b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/bootstrap_microservice.py new file mode 100644 index 00000000..32073455 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/bootstrap_microservice.py @@ -0,0 +1,75 @@ +import os +import sys + +from microservice.save_environment_variables import save_environment_variables + + +def _get_all_parent_dirs(path): + while True: + parent, base = os.path.split(path) + if base: + yield path + if not parent or path == parent: + break + path = parent + yield '' + + +def _get_all_parent_dirs_with_combinations(path_1, path_2): + for path in _get_all_parent_dirs(os.path.join(path_1, path_2)): + yield path + + for path in _get_all_parent_dirs(os.path.join(path_2, path_1)): + yield path + + for path in _get_all_parent_dirs(path_1): + yield path + + for path in _get_all_parent_dirs(path_2): + yield path + + +def _nesting_level(path): + return path.rstrip('/').count('/') + + +def _generate_config_full_path(base_path, config_dir, config_dirs_combinations): + image_config_dirs = [os.path.join(base_path, config_dir, path) for path in config_dirs_combinations] + image_config_dirs.sort(key=_nesting_level, reverse=True) + return image_config_dirs + + +def main(): + if "CONFIG_DIR" in os.environ: + service_path = os.path.join("/opt", os.environ["MICROSERVICE_NAME"]) + config_dir = os.environ["CONFIG_DIR"] + microservice_env = os.environ.get("MICROSERVICE_ENV", '') + microservice_app_id = os.environ.get("MICROSERVICE_APP_ID", '') + + config_dirs_combinations = set(_get_all_parent_dirs_with_combinations(microservice_env, microservice_app_id)) + service_config_dirs_full_paths = _generate_config_full_path(service_path, config_dir, config_dirs_combinations) + + image_name = os.environ.get("IMAGE_NAME") + if image_name and image_name != os.environ["MICROSERVICE_NAME"]: + image_path = os.path.join("/opt", image_name) + image_config_dirs = _generate_config_full_path(image_path, config_dir, config_dirs_combinations) + service_config_dirs_full_paths.extend(image_config_dirs) + + config_dirs_existing_paths = list(filter(os.path.isdir, service_config_dirs_full_paths)) + + local_config_path = os.pathsep.join(config_dirs_existing_paths) + + if "CONFIG_PATH" in os.environ: + os.environ["CONFIG_PATH"] = os.environ["CONFIG_PATH"] + os.pathsep + local_config_path + else: + os.environ["CONFIG_PATH"] = local_config_path + + save_environment_variables() + supervisor_cmd = "supervisord" + os.execvp(supervisor_cmd, (supervisor_cmd, "-c", "/etc/supervisor/supervisord.conf")) + + +if __name__ == '__main__': + print('WARNING: Calling this script directly has been deprecated. Try `microservice bootstrap` instead.', + file=sys.stderr) + main() diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/common/__init__.py b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/common/consul.py b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/common/consul.py new file mode 100644 index 00000000..46ce8164 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/common/consul.py @@ -0,0 +1,35 @@ +import json +import sys + +import requests +from microservice.common import docker_client + +_CONSUL_TIMEOUT_IN_SECONDS = 7 +_SHIP_IP = None + + +def print_err(*objs): + print(*objs, file=sys.stderr) + + +def get_consul_url(): + hostname = docker_client.get_ship_ip() + url = 'http://{}:8500/v1/'.format(hostname) + return url + + +def consul_query(query): + return json.loads(consul_get(query).text) + + +def consul_get(query): + return requests.get(get_consul_url() + query, timeout=_CONSUL_TIMEOUT_IN_SECONDS) + + +def consul_post(query, data): + return requests.post(get_consul_url() + query, data=json.dumps(data), timeout=_CONSUL_TIMEOUT_IN_SECONDS) + + +def consul_put(query, data=None): + data = data or {} + return requests.put(get_consul_url() + query, data=json.dumps(data), timeout=_CONSUL_TIMEOUT_IN_SECONDS) diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/common/docker_client.py b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/common/docker_client.py new file mode 100644 index 00000000..1f827e8a --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/common/docker_client.py @@ -0,0 +1,29 @@ +import socket + +import docker + +DOCKER_API_VERSION = '1.24' +DOCKER_SOCKET_PATH = '/var/run/docker.sock' + + +def api(): + return docker.APIClient(base_url='unix://' + DOCKER_SOCKET_PATH, version=DOCKER_API_VERSION, timeout=7) + + +def get_docker_inspect(container_id): + docker_api = api() + docker_inspect = docker_api.inspect_container(container_id) + return docker_inspect + + +_SHIP_IP = None + + +def get_ship_ip(): + global _SHIP_IP + if _SHIP_IP is None: + container_id = socket.gethostname() + docker_inspect = get_docker_inspect(container_id) + gateway_ip = docker_inspect['NetworkSettings']['Gateway'] + _SHIP_IP = gateway_ip + return _SHIP_IP diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/common/service_discovery.py b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/common/service_discovery.py new file mode 100644 index 00000000..792464fa --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/common/service_discovery.py @@ -0,0 +1,62 @@ +import requests + +from microservice.common.consul import print_err +from microservice.defines import ARMADA_API_URL + + +class UnsupportedArmadaApiException(Exception): + pass + + +def get_services(params=None): + response = requests.get(ARMADA_API_URL + '/list', params=params).json() + return response['result'] + + +def get_service_to_addresses(): + service_to_addresses = {} + services = get_services() + for service in services: + if service['status'] not in ('passing', 'warning'): + continue + tags = service['tags'] + service_index = (service['name'], tags.get('env'), tags.get('app_id')) + if service_index not in service_to_addresses: + service_to_addresses[service_index] = [] + service_to_addresses[service_index].append(service['address']) + return service_to_addresses + + +def register_service_in_armada(microservice_id, microservice_name, microservice_port, microservice_tags, + container_created_timestamp, single_active_instance): + post_data = { + 'microservice_id': microservice_id, + 'microservice_name': microservice_name, + 'microservice_port': microservice_port, + 'microservice_tags': microservice_tags, + 'container_created_timestamp': container_created_timestamp, + 'single_active_instance': single_active_instance, + } + response = requests.post(ARMADA_API_URL + '/register', json=post_data) + response.raise_for_status() + + +def register_service_in_armada_v1(microservice_id, microservice_name, microservice_local_port, microservice_env, + microservice_app_id, container_created_timestamp, single_active_instance, + microservice_version): + if '/' not in microservice_local_port: + microservice_local_port = '{}/tcp'.format(microservice_local_port) + post_data = { + 'microservice_name': microservice_name, + 'microservice_local_port': microservice_local_port, + 'microservice_env': microservice_env, + 'microservice_app_id': microservice_app_id, + 'container_created_timestamp': container_created_timestamp, + 'single_active_instance': single_active_instance, + 'microservice_version': microservice_version, + } + url = '{}/v1/local/register/{}'.format(ARMADA_API_URL, microservice_id) + response = requests.post(url, json=post_data) + if response.status_code == 404: + raise UnsupportedArmadaApiException('Endpoint /v1/local/register is unavailable.') + response.raise_for_status() diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/defines.py b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/defines.py new file mode 100644 index 00000000..ff6e00db --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/defines.py @@ -0,0 +1,12 @@ +from subprocess import check_output + + +def get_armada_host_ip(): + output = check_output(['ip', 'route']).decode() + for line in output.splitlines(): + if line.startswith('default'): + return line.split()[2] + return '172.17.0.1' + + +ARMADA_API_URL = 'http://{}:8900'.format(get_armada_host_ip()) diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/exceptions.py b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/exceptions.py new file mode 100644 index 00000000..4fa4deff --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/exceptions.py @@ -0,0 +1,2 @@ +class ArmadaApiServiceNotFound(Exception): + pass diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/local_magellan/__init__.py b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/local_magellan/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/local_magellan/haproxy.py b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/local_magellan/haproxy.py new file mode 100644 index 00000000..1a729604 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/local_magellan/haproxy.py @@ -0,0 +1,76 @@ +import os +import socket + +CONFIG_PATH = '/var/opt/haproxy-local.cfg' +PID_PATH = '/var/run/haproxy-local.pid' +CONFIG_HEADER = ''' +global + daemon + maxconn 1024 + +defaults + mode tcp + timeout connect 7s + timeout server 24d + timeout client 24d + +''' + + +def _is_ip(hostname): + try: + socket.inet_aton(hostname) + return True + except socket.error: + return False + + +def generate_config_from_mapping(port_to_addresses): + result = CONFIG_HEADER + for port, addresses in port_to_addresses.items(): + result += '\tlisten service_{port}\n'.format(**locals()) + result += '\t\tbind :::{port} v4v6\n'.format(**locals()) + if not addresses: + result += '\t\ttcp-request connection reject\n' + else: + result += _make_server_config(addresses) + result += '\n' + return result + + +def _make_server_config(addresses): + result = "" + for i, address in enumerate(addresses): + protocol, host = address.split("://", 2) if "://" in address else ("", address) + + result += '\t\tserver server_{i} {host} maxconn 128\n'.format(**locals()) + hostname = host.split(':')[0] + if protocol == 'http': + result += '\t\thttp-request del-header Proxy\n' + if not _is_ip(hostname): + result += '\t\thttp-request set-header Host {}\n'.format(host) + result += '\t\tmode {}\n'.format(protocol) + return result + + +def put_config(config): + with open(CONFIG_PATH, 'w') as haproxy_config_file: + haproxy_config_file.write(config) + + +def restart(): + try: + with open(PID_PATH, 'r') as pid_file: + pid = pid_file.read() + except IOError: + pid = None + command = 'haproxy -f {config_path} -p {pid_path}'.format(config_path=CONFIG_PATH, pid_path=PID_PATH) + if pid: + command += ' -sf {pid}'.format(pid=pid) + os.system(command) + + +def update_from_mapping(port_to_addresses): + new_config = generate_config_from_mapping(port_to_addresses) + put_config(new_config) + restart() diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/local_magellan/local_magellan.py b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/local_magellan/local_magellan.py new file mode 100644 index 00000000..e63343d9 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/local_magellan/local_magellan.py @@ -0,0 +1,106 @@ +import glob +import json +import os +import random +import sys +import time + +from microservice.common import service_discovery +from microservice.local_magellan import haproxy + +MICROSERVICE_ENV = os.environ.get('MICROSERVICE_ENV') or None +MICROSERVICE_APP_ID = os.environ.get('MICROSERVICE_APP_ID') or None +LOCAL_MAGELLAN_CONFIG_DIR_PATH = '/var/opt/local-magellan/' +SERVICE_DISCOVERY_CONFIG_PATH = '/var/opt/service_discovery.json' +SERVICE_TO_ADDRESSES_CONFIG_DIR_PATH = '/var/opt/service_to_addresses.json' + + +def print_err(*objs): + print(*objs, file=sys.stderr) + + +def save_magellan_config(magellan_config): + try: + os.makedirs(LOCAL_MAGELLAN_CONFIG_DIR_PATH) + except OSError: + if not os.path.isdir(LOCAL_MAGELLAN_CONFIG_DIR_PATH): + raise + port = list(magellan_config)[0] + config_file_name = '{0}.json'.format(port) + config_file_path = os.path.join(LOCAL_MAGELLAN_CONFIG_DIR_PATH, config_file_name) + with open(config_file_path, 'w') as f: + f.write(json.dumps(magellan_config)) + + +def read_magellan_configs(): + result = {} + for config_file_path in glob.glob(os.path.join(LOCAL_MAGELLAN_CONFIG_DIR_PATH, '*.json')): + with open(config_file_path) as f: + result.update(json.load(f)) + return result + + +def match_port_to_addresses(port_to_services, service_to_addresses): + port_to_addresses = {} + for port, service_dict in port_to_services.items(): + service_envs = [] + potential_service_env = '' + env = service_dict.get('env') + if env: + for part in env.split('/'): + potential_service_env += part + service_envs.append(potential_service_env) + potential_service_env += '/' + else: + service_envs = [env] + + port_to_addresses[port] = [] + for service_env in reversed(service_envs): + service_tuple = (service_dict['microservice_name'], service_env, service_dict.get('app_id')) + if service_tuple in service_to_addresses: + port_to_addresses[port] = service_to_addresses[service_tuple] + break + return port_to_addresses + + +def has_data_changed(port_to_addresses): + if os.path.exists(SERVICE_TO_ADDRESSES_CONFIG_DIR_PATH): + with open(SERVICE_TO_ADDRESSES_CONFIG_DIR_PATH, 'r') as f: + old_config = json.load(f) + if old_config == port_to_addresses: + return False + return True + + +def main(): + time.sleep(1) + first_try = True + while True: + try: + port_to_services = read_magellan_configs() + if not port_to_services and not first_try: + sys.exit(0) + + with open(SERVICE_DISCOVERY_CONFIG_PATH, 'w') as f: + json.dump(port_to_services, f) + + service_to_addresses = service_discovery.get_service_to_addresses() + port_to_addresses = match_port_to_addresses(port_to_services, service_to_addresses) + + if has_data_changed(port_to_addresses): + with open(SERVICE_TO_ADDRESSES_CONFIG_DIR_PATH, 'w') as f: + json.dump(port_to_addresses, f, indent=4, sort_keys=True) + haproxy.update_from_mapping(port_to_addresses) + + except Exception as e: + print_err("ERROR on updating haproxy: {exception_class} - {exception}".format( + exception_class=type(e).__name__, + exception=str(e))) + first_try = False + time.sleep(10 + random.uniform(-2, 2)) + + +if __name__ == '__main__': + print('WARNING: Calling this script directly has been deprecated. Try `microservice local-magellan` instead.', + file=sys.stderr) + main() diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/local_magellan/require_service.py b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/local_magellan/require_service.py new file mode 100644 index 00000000..a66bd0a1 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/local_magellan/require_service.py @@ -0,0 +1,83 @@ +import argparse +import logging +import os +import sys + +from microservice.local_magellan import local_magellan + +from armada import hermes + + +def print_err(*objs): + print(*objs, file=sys.stderr) + + +def parse_args(): + parser = argparse.ArgumentParser(description='Make given microservice available at local HAProxy.') + add_arguments(parser) + return parser.parse_args() + + +def add_arguments(parser): + parser.add_argument('port', nargs='?', + type=int, + help='Bind port. Given microservice will be available at localhost:port.') + parser.add_argument('microservice_name', nargs='?', + help='Name of the microservice.') + parser.add_argument('--env', + help='Environment of the microservice. Default: environment variable $MICROSERVICE_ENV.') + parser.add_argument('--app_id', + help='Application ID of the microservice. Default: environment variable $MICROSERVICE_APP_ID.') + parser.add_argument('-c', '--config', + help='Name of file with configuration. It should be located in config directory.') + + +def create_magellan_config_from_file(file_name): + config = hermes.get_config(file_name) + + if config: + for microservice_name, configurations in config.items(): + if isinstance(configurations, list): + for configuration in configurations: + configure_single_requirement(microservice_name, **configuration) + + elif isinstance(configurations, dict): + configure_single_requirement(microservice_name, **configurations) + else: + logging.warning('Empty dependency file: {}'.format(file_name)) + + +def configure_single_requirement(microservice_name, port, env=None, app_id=None): + microservice = {'microservice_name': microservice_name} + + if env is None: + env = os.environ.get('MICROSERVICE_ENV') + if env: + microservice['env'] = env + + if app_id is None: + app_id = os.environ.get('MICROSERVICE_APP_ID') + if app_id: + microservice['app_id'] = app_id + + magellan_config = {port: microservice} + local_magellan.save_magellan_config(magellan_config) + + +def main(args): + file_name = args.config + + if not file_name: + if not args.port or not args.microservice_name: + raise RuntimeError('port and microservice_name are required') + else: + configure_single_requirement(args.microservice_name, args.port, args.env, args.app_id) + else: + create_magellan_config_from_file(file_name) + + +if __name__ == '__main__': + print('WARNING: Calling this script directly has been deprecated. Try `microservice require` instead.', + file=sys.stderr) + args = parse_args() + main(args) diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/microservice b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/microservice new file mode 100644 index 00000000..6dcc68f7 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/microservice @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 + +from abc import ABCMeta, abstractmethod +from argparse import ArgumentParser + +from microservice import bootstrap_microservice, armada_agent, register_in_service_discovery, run_hooks, version +from microservice.local_magellan import require_service, local_magellan + + +class Command(metaclass=ABCMeta): + help = '' + + def __init__(self, parsers): + parser = parsers.add_parser(self.name, help=self.help, description=self.help) + self.add_args(parser) + parser.set_defaults(func=self.execute) + + @property + @abstractmethod + def name(self): + pass + + @abstractmethod + def execute(self, args): + pass + + def add_args(self, parser): + pass + + +class BootstrapCommand(Command): + help = """Command for bootstrapping microservice. It's meant to be used as the entrypoint of the container.""" + + @property + def name(self): + return 'bootstrap' + + def execute(self, args): + bootstrap_microservice.main() + + +class RequireCommand(Command): + help = """Add requirement for dependent services. It will bind them to some localhost ports.""" + + @property + def name(self): + return 'require' + + def execute(self, args): + require_service.main(args) + + def add_args(self, parser): + require_service.add_arguments(parser) + + +class AgentCommand(Command): + help = """Run armada agent, that takes care of registering service and performing periodic health-checks.""" + + @property + def name(self): + return 'agent' + + def execute(self, args): + armada_agent.main() + + +class RegisterCommand(Command): + help = """Register service or subservice in armada's service-discovery catalog.""" + + @property + def name(self): + return 'register' + + def execute(self, args): + register_in_service_discovery.main(args) + + def add_args(self, parser): + register_in_service_discovery.add_arguments(parser) + + +class HooksCommand(Command): + help = """Run hooks inside the microservice.""" + + @property + def name(self): + return 'hooks' + + def execute(self, args): + run_hooks.main(args) + + def add_args(self, parser): + run_hooks.add_arguments(parser) + + +class LocalMagellanCommand(Command): + help = """Run local-magellan.""" + + @property + def name(self): + return 'local-magellan' + + def execute(self, args): + local_magellan.main() + + +def parse_args(): + ap = ArgumentParser() + ap.add_argument('-V', '--version', action='version', version=version.VERSION) + subparsers = ap.add_subparsers(dest='subparser_command') + commands = [ + BootstrapCommand, RequireCommand, AgentCommand, RegisterCommand, HooksCommand, LocalMagellanCommand, + ] + for command in commands: + command(subparsers) + return ap.parse_args() + + +def main(): + args = parse_args() + if hasattr(args, 'func'): + args.func(args) + + +if __name__ == '__main__': + main() diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/register_in_service_discovery.py b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/register_in_service_discovery.py new file mode 100644 index 00000000..37513e51 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/register_in_service_discovery.py @@ -0,0 +1,84 @@ +import argparse +import json +import os +import re +import socket +import sys + +from microservice.common.docker_client import get_docker_inspect + +REGISTRATION_DIRECTORY = "/var/opt/service-registration/" +PORT_PATTERN = re.compile(r'^(\d+)(?:/(tcp|udp))?$', re.IGNORECASE) + + +def _parse_args(): + parser = argparse.ArgumentParser(description='Register service in Armada.') + add_arguments(parser) + return parser.parse_args() + + +def add_arguments(parser): + parser.add_argument('port', + default='80', + nargs='?', + help='Local TCP (default), or UDP port of the registered service. ' + 'Examples: 80, 8080/tcp, 6001/udp. Default 80/tcp.') + parser.add_argument('-s', '--subservice', + help='Name of the subservice. It will be visible in Armada as: ' + '[microservice_name]:[subservice_name].') + parser.add_argument('-c', '--health_check', help="Alternative health check path for this service.", default=None) + parser.add_argument('--single-active-instance', action='store_true', + help="Service discovery mechanisms will return max. 1 working instance of such service. " + "The rest will have status 'standby'.", + default=False) + + +def _create_service_file(service_filename, service_registration_data): + service_file_path = REGISTRATION_DIRECTORY + service_filename + ".json" + with open(service_file_path, "w+") as f: + json.dump(service_registration_data, f) + + +def _get_port_and_protocol(args_port): + m = PORT_PATTERN.match(args_port) + if m is None: + raise ValueError('Incorrect format of --port argument. It should match regexp: ^(\d+)(?:/(tcp|udp))?$') + port, protocol = m.groups() + port = int(port) + protocol = (protocol or 'tcp').lower() + return '{}/{}'.format(port, protocol) + + +def main(args): + container_id = socket.gethostname() + docker_inspect = get_docker_inspect(container_id) + port_and_protocol = _get_port_and_protocol(args.port) + service_port = int(docker_inspect['NetworkSettings']['Ports'][port_and_protocol][0]['HostPort']) + service_filename = microservice_name = os.environ.get('MICROSERVICE_NAME') + + service_id = container_id + full_service_name = microservice_name + if args.subservice: + service_id += ':' + args.subservice + full_service_name += ':' + args.subservice + service_filename += '-' + args.subservice + + service_data = { + "service_id": service_id, + "service_port": service_port, + "service_name": full_service_name, + "service_container_port": args.port, + "single_active_instance": args.single_active_instance, + } + + if args.health_check: + service_data["service_health_check_path"] = args.health_check + + _create_service_file(service_filename, service_data) + + +if __name__ == '__main__': + print('WARNING: Calling this script directly has been deprecated. Try `microservice register` instead.', + file=sys.stderr) + args = _parse_args() + main(args) diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/run_hooks.py b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/run_hooks.py new file mode 100644 index 00000000..5da007ee --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/run_hooks.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 + +import argparse +import errno +import logging +import os +import subprocess +import sys +from glob import glob +from logging.handlers import TimedRotatingFileHandler + +REGISTERED_HOOKS = ['pre-stop'] +HOOKS_PATH_WILDCARD = '/opt/*/hooks' +HOOKS_LOG_PATH = '/var/log/armada/hooks' + + +def _parse_args(): + parser = argparse.ArgumentParser(description='Run hook inside the microservice.') + add_arguments(parser) + return parser.parse_args() + + +def add_arguments(parser): + parser.add_argument('hook_name', choices=REGISTERED_HOOKS, help='Name of the hook.') + + +def _get_hook_files(hook_name): + global HOOKS_PATH_WILDCARD + files = os.path.join(HOOKS_PATH_WILDCARD, hook_name, '*') + for hook_file_path in sorted(glob(files)): + if not os.path.isdir(hook_file_path): + yield hook_file_path + + +def _mkdir(path): + try: + os.makedirs(path) + except OSError as exc: + if exc.errno != errno.EEXIST or not os.path.isdir(path): + raise + + +def _get_logger(logger_name): + global HOOKS_LOG_PATH + logger = logging.getLogger(logger_name) + logger.setLevel(logging.INFO) + formatter = logging.Formatter( + '%(levelname)s - %(asctime)s - %(name)s - %(message)s', '%Y-%m-%d %H:%M:%S' + ) + + if not os.path.exists(HOOKS_LOG_PATH): + _mkdir(HOOKS_LOG_PATH) + + path = os.path.join(HOOKS_LOG_PATH, '{}.log'.format(logger_name)) + handler = TimedRotatingFileHandler(path, when='midnight', backupCount=3) + handler.setFormatter(formatter) + logger.addHandler(handler) + return logger + + +def run_hook(hook_name): + logger = _get_logger(hook_name) + for path in _get_hook_files(hook_name): + os.chmod(path, 0o755) + os.system('sync') + logger.info('Executing hook: {}'.format(path)) + try: + output = subprocess.check_output([path]) + except (OSError, subprocess.CalledProcessError) as e: + logger.error('Error: {}'.format(e)) + else: + logger.info('result: {}'.format(output)) + + +def main(args): + run_hook(args.hook_name) + + +if __name__ == "__main__": + print('WARNING: Calling this script directly has been deprecated. Try `microservice hooks` instead.', + file=sys.stderr) + args = _parse_args() + main(args) diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/save_environment_variables.py b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/save_environment_variables.py new file mode 100644 index 00000000..8d8ad21f --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/save_environment_variables.py @@ -0,0 +1,77 @@ +import os +import sys + +EXCLUDED_ENVIRONMENT_KEYS = { + 'PATH', + 'HOME', + 'SUPERVISOR_GROUP_NAME', + 'SUPERVISOR_ENABLED', + 'SUPERVISOR_PROCESS_NAME', + 'SUPERVISOR_SERVER_URL' +} +EXCLUDED_ENVIRONMENT_KEYS_FROM_CRONTAB = { + 'RESTART_CONTAINER_PARAMETERS', + 'ARMADA_RUN_COMMAND', + 'MICROSERVICE_FORCE_APT_GET_UPDATE' +} +BASHRC_PATH = '/etc/bash.bashrc' +ARMADA_ENVIRONMENT_VARIABLES_PATH = '/var/opt/armada_environment.sh' +ARMADA_ENVIRONMENT_VARIABLES_EXPORT_PATH = '/var/opt/armada_environment_export.sh' + + +def parse_environment_variables(environment_variables): + for env_var in environment_variables: + env_key, env_val = env_var.split('=', 1) + yield env_key, env_val + + +def exclude_environment_variables(environment_keys_values, excluded): + return [env_var for env_var in environment_keys_values if env_var[0] not in excluded] + + +def add_environment_variables_to_bashrc(environment_variables_export_path, bashrc_path): + with open(bashrc_path, 'a') as bashrc: + line = 'source {environment_variables_export_path}\n'.format(**locals()) + bashrc.write(line) + + +def create_safe_env_var_definition(env_key, env_val): + return '{env_key}="{env_val}"'.format(**locals()) + + +def create_armada_environment_variables_file(environment_keys_values, environment_variables_path): + with open(environment_variables_path, 'w') as environment_variables_file: + for env_key, env_val in environment_keys_values: + safe_env_var = create_safe_env_var_definition(env_key, env_val) + environment_variables_file.write(safe_env_var + '\n') + + +def create_armada_environment_variables_export_file(environment_keys_values, environment_variables_export_path): + with open(environment_variables_export_path, 'w') as environment_variables_export_file: + for env_key, env_val in environment_keys_values: + safe_env_var = 'export ' + create_safe_env_var_definition(env_key, env_val) + environment_variables_export_file.write(safe_env_var + '\n') + + +def add_environment_variables_to_crontab(environment_keys_values): + for env_key, env_val in environment_keys_values: + safe_env_var = create_safe_env_var_definition(env_key, env_val) + command = '(echo \'{0}\'; crontab -l) | crontab -'.format(safe_env_var) + if os.system(command) != 0: + print('Following environment variable could not have been added to crontab, possibly because of ' + 'hitting crontab\'s 1000 characters limit per line:\n{0}'.format(safe_env_var), file=sys.stderr) + + +def save_environment_variables(): + environment_keys_values = [(key, os.environ[key]) for key in os.environ if key not in EXCLUDED_ENVIRONMENT_KEYS] + + create_armada_environment_variables_file(environment_keys_values, + ARMADA_ENVIRONMENT_VARIABLES_PATH) + create_armada_environment_variables_export_file(environment_keys_values, + ARMADA_ENVIRONMENT_VARIABLES_EXPORT_PATH) + + add_environment_variables_to_bashrc(ARMADA_ENVIRONMENT_VARIABLES_EXPORT_PATH, BASHRC_PATH) + + environment_keys_values_filtered_for_crontab = exclude_environment_variables(environment_keys_values, + EXCLUDED_ENVIRONMENT_KEYS_FROM_CRONTAB) + add_environment_variables_to_crontab(environment_keys_values_filtered_for_crontab) diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/version.py b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/version.py new file mode 100644 index 00000000..7723ca46 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/microservice/version.py @@ -0,0 +1 @@ +VERSION = "0.0.0" diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/scripts/tools/.gitkeep b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/scripts/tools/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/scripts/tools/acd.bashrc b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/scripts/tools/acd.bashrc new file mode 100644 index 00000000..ceeb11e5 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/scripts/tools/acd.bashrc @@ -0,0 +1,7 @@ +## acd +## Description: changes cwd to /opt/$IMAGE_NAME/[DIRECTORY] +## Usage: acd [DIRECTORY] +## +function acd { + cd /opt/${IMAGE_NAME}/${1} +} diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/scripts/tools/actl.bashrc b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/scripts/tools/actl.bashrc new file mode 100644 index 00000000..e4503f23 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/scripts/tools/actl.bashrc @@ -0,0 +1,5 @@ +## actl +## Description: alias for supervisorctl +## Usage: actl [ACTION] [SERVICE] +## +alias actl="supervisorctl" diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/scripts/tools/ahelp.bashrc b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/scripts/tools/ahelp.bashrc new file mode 100644 index 00000000..20b6d060 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/scripts/tools/ahelp.bashrc @@ -0,0 +1,4 @@ +function ahelp { + local DIRTOOL="/opt/microservice/scripts/tools" + cat $DIRTOOL/* | grep -E '^##' | cut -c '3-' +} diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/scripts/tools/alogs.sh b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/scripts/tools/alogs.sh new file mode 100644 index 00000000..befa0fe1 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/scripts/tools/alogs.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +## alogs +## Description: opens mc with supervisor logs in one pane and /opt/$IMAGE_NAME dir in second +## Usage: alogs +## +exec mc /var/log/supervisor "/opt/${IMAGE_NAME}" diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/scripts/tools/atail.sh b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/scripts/tools/atail.sh new file mode 100644 index 00000000..fe25477a --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/scripts/tools/atail.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +## atail +## Description: runs [tail -f] for /var/log/supervisor files +## Usage: atail [FILENAME] +## +exec tail -f "/var/log/supervisor/${1}"* diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/scripts/tools/microservice_bashrc.source b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/scripts/tools/microservice_bashrc.source new file mode 100644 index 00000000..71925225 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/scripts/tools/microservice_bashrc.source @@ -0,0 +1,8 @@ +#!/bin/bash + +source /opt/microservice/scripts/tools/acd.bashrc +source /opt/microservice/scripts/tools/actl.bashrc +source /opt/microservice/scripts/tools/ahelp.bashrc +source /etc/bash_completion_armada/acd +source /etc/bash_completion_armada/atail +source /etc/bash_completion_armada/supervisorctl diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/setup.py b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/setup.py new file mode 100644 index 00000000..06e4d5e4 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/setup.py @@ -0,0 +1,25 @@ +from microservice.version import VERSION +from setuptools import setup + +setup( + name='armada-microservice', + description='Microservice package - base of all armada microservices. CLI for communicating between ' + 'microservice and armada.', + version=VERSION, + author='Cerebro', + author_email='cerebro@ganymede.eu', + packages=[ + 'microservice', + 'microservice.common', + 'microservice.local_magellan', + ], + scripts=[ + 'microservice/microservice', + ], + install_requires=[ + 'docker==2.4.2', + 'requests==2.29.0', + 'armada', + 'pyopenssl', + ], +) diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/ssh/authorized_keys b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/ssh/authorized_keys new file mode 100644 index 00000000..07907cee --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/ssh/authorized_keys @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFysjfUHkkM4wHMZLDJdVNZe19sAe5CzD0Porepc8DPiTvyuJDPpMaucfeFnHtd4KshRr4l358Dka6XxgPcvHvlZT9BBIzCkhnjZRwZ4bjNM+RG/dDF69i4K7W9MzGt+1g38HhdZBuL02gMcxe9FCVu27ahFWYwoZa/UhponPYuQClz1m9rwE0JkrEQsmimwTKaZLpDL6905sMiU4YnQQYcRKtnNt/8XDVpRlYYAMF3qJ6l2b/PpoQiMJ5ieYyJOMyIid2VLPi43R6QNaSnZvzLov19NAlc15BHs1qVmMi62gzHUyqQ/afadV+Au8CyPTxqCqzyPTSFlE9zrlmxtq/ root \ No newline at end of file diff --git a/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/ssh/docker_id_rsa.pub b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/ssh/docker_id_rsa.pub new file mode 100644 index 00000000..e9643473 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/opt/microservice/ssh/docker_id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFysjfUHkkM4wHMZLDJdVNZe19sAe5CzD0Porepc8DPiTvyuJDPpMaucfeFnHtd4KshRr4l358Dka6XxgPcvHvlZT9BBIzCkhnjZRwZ4bjNM+RG/dDF69i4K7W9MzGt+1g38HhdZBuL02gMcxe9FCVu27ahFWYwoZa/UhponPYuQClz1m9rwE0JkrEQsmimwTKaZLpDL6905sMiU4YnQQYcRKtnNt/8XDVpRlYYAMF3qJ6l2b/PpoQiMJ5ieYyJOMyIid2VLPi43R6QNaSnZvzLov19NAlc15BHs1qVmMi62gzHUyqQ/afadV+Au8CyPTxqCqzyPTSFlE9zrlmxtq/ \ No newline at end of file diff --git a/docker-containers/microservice_jammy/packaging/microservice/usr/local/bin/alogs b/docker-containers/microservice_jammy/packaging/microservice/usr/local/bin/alogs new file mode 100644 index 00000000..91976cee --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/usr/local/bin/alogs @@ -0,0 +1 @@ +../../../opt/microservice/scripts/tools/alogs.sh \ No newline at end of file diff --git a/docker-containers/microservice_jammy/packaging/microservice/usr/local/bin/atail b/docker-containers/microservice_jammy/packaging/microservice/usr/local/bin/atail new file mode 100644 index 00000000..bd783d6d --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/microservice/usr/local/bin/atail @@ -0,0 +1 @@ +../../../opt/microservice/scripts/tools/atail.sh \ No newline at end of file diff --git a/docker-containers/microservice_jammy/packaging/package_build.py b/docker-containers/microservice_jammy/packaging/package_build.py new file mode 100644 index 00000000..f785f5a1 --- /dev/null +++ b/docker-containers/microservice_jammy/packaging/package_build.py @@ -0,0 +1,73 @@ +#!/usr/bin/python3 + +from argparse import ArgumentParser +import subprocess + + +def main(): + parser = ArgumentParser() + parser.add_argument('--version', required=True) + args = parser.parse_args() + version = args.version + _create_package(version) + + +def _create_package(version): + + options = { + 'depends': [ + "supervisor", + "python3", + "python3-dev", + "python3-pip", + "git", + "curl", + "mc", + "less", + "software-properties-common", + "wget", + "vim", + "gcc", + "unzip", + "apt-utils", + "net-tools", + "cron", + "netcat", + "sudo", + "file", + "iproute2", + "bash-completion" + ], + 'suggests': [ + "haproxy" + ] + } + + fpm_options = [ + "fpm", + "-t", "deb", + "-s", "dir", + "--description", "armada", + "-C", './microservice', + "--license", "\"Apache 2.0\"", + "--maintainer", "cerebro@ganymede.eu", + "--url", "armada.sh", + "--after-install", 'after-install.sh', + "--after-remove", 'after-remove.sh', + "--template-scripts", + "--name", 'armada-microservice', + "--version", version, + "--architecture", 'x86_64', + ] + + for dep in options['depends']: + fpm_options += ['--depends', dep] + for dep in options['suggests']: + fpm_options += ['--deb-suggests', dep] + + subprocess.check_call(fpm_options) + print('OK') + + +if __name__ == '__main__': + main() diff --git a/docker-containers/microservice_php_jammy/Dockerfile b/docker-containers/microservice_php_jammy/Dockerfile new file mode 100644 index 00000000..4f4bcb57 --- /dev/null +++ b/docker-containers/microservice_php_jammy/Dockerfile @@ -0,0 +1,30 @@ +FROM microservice_jammy +MAINTAINER Cerebro + +RUN apt-get update +RUN apt-get install -y apache2 logrotate php8.1 php8.1-mysql php8.1-curl php8.1-mbstring php8.1-zip wget + +RUN ln -s /etc/php/8.1 /etc/php8 + +ADD ./supervisor/* /etc/supervisor/conf.d/ + +# Enable cron. +RUN echo "Etc/UTC" > /etc/timezone && dpkg-reconfigure -f noninteractive tzdata + +# Apache configuration. +ADD ./apache2_vhost.conf /etc/apache2/sites-available/apache2_vhost.conf +RUN ln -s /etc/apache2/sites-available/apache2_vhost.conf /etc/apache2/sites-enabled/apache2_vhost.conf +RUN rm -f /etc/apache2/sites-enabled/000-default.conf +RUN echo "StartServers 1\nMinSpareServers 1\nMaxSpareServers 3" >> /etc/apache2/apache2.conf + +# Remove old session files +RUN (crontab -l; echo "0 2 * * * /usr/lib/php/sessionclean") | crontab - + +# Enable logrotate +RUN useradd syslog +RUN (crontab -l; echo "@daily /usr/sbin/logrotate /etc/logrotate.conf") | crontab - + +ADD . /opt/microservice_php +RUN chmod +x /opt/microservice_php/run_apache2.sh + +EXPOSE 80 diff --git a/docker-containers/microservice_php_jammy/apache2_vhost.conf b/docker-containers/microservice_php_jammy/apache2_vhost.conf new file mode 100644 index 00000000..1cf7fa52 --- /dev/null +++ b/docker-containers/microservice_php_jammy/apache2_vhost.conf @@ -0,0 +1,11 @@ + + + DocumentRoot /opt/www/www + + + Options FollowSymLinks + AllowOverride All + Require all granted + + + diff --git a/docker-containers/microservice_php_jammy/health-checks/http-ok b/docker-containers/microservice_php_jammy/health-checks/http-ok new file mode 100644 index 00000000..c25e5165 --- /dev/null +++ b/docker-containers/microservice_php_jammy/health-checks/http-ok @@ -0,0 +1,13 @@ +#!/bin/bash + +url=http://localhost +# -c /dev/null is for websites that redirect with new cookies set. +http_status_code=$(curl -sL -w "%{http_code}" -o /dev/null -c /dev/null ${url}) + +if [ ${http_status_code} -ne '200' ]; then + echo "HTTP health check failed" + exit 2 +fi + +echo "HTTP health check OK" +exit 0 diff --git a/docker-containers/microservice_php_jammy/run_apache2.sh b/docker-containers/microservice_php_jammy/run_apache2.sh new file mode 100755 index 00000000..4d0491c6 --- /dev/null +++ b/docker-containers/microservice_php_jammy/run_apache2.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -e +source /etc/apache2/envvars +exec apache2 -c "ErrorLog /dev/stdout" -DFOREGROUND diff --git a/docker-containers/microservice_php_jammy/src/hermes.php b/docker-containers/microservice_php_jammy/src/hermes.php new file mode 100644 index 00000000..6e480598 --- /dev/null +++ b/docker-containers/microservice_php_jammy/src/hermes.php @@ -0,0 +1,31 @@ +